phlex-reactive 0.9.5 → 0.10.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.
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["reactive_controller.js"],
4
4
  "sourcesContent": [
5
- "import { Controller } from \"@hotwired/stimulus\"\n// Import the BARE specifier the engine already pins (phlex/reactive/confirm),\n// NOT a relative \"./confirm.js\" (issue #57). Under importmap-rails + Propshaft\n// the controller is served at its DIGESTED url; a relative sibling import is\n// left untouched (Propshaft rewrites only RAILS_ASSET_URL(...), and the import\n// map resolves ONLY bare specifiers), so \"./confirm.js\" resolves against the\n// digested controller url → an undigested /assets/.../confirm.js that 404s, and\n// the throwing import takes down every Stimulus controller on the page. The\n// bare specifier resolves to the digested asset through the import map, and\n// bundlers/bun resolve it the same way they already resolve\n// \"phlex/reactive/reactive_controller\" (see tsconfig.json paths for the tests).\nimport { confirmResolver } from \"phlex/reactive/confirm\"\n// Client-side computes (data bindings): the reducer registry behind\n// reactive_compute. Bare specifier for the same import-map reason as confirm.\nimport { computeReducer } from \"phlex/reactive/compute\"\n\n// The ONE generic controller behind every reactive Phlex component. It\n// replaces the per-feature Stimulus controllers you'd otherwise hand-write\n// for interactive components. A component declares its actions in Ruby (via\n// Phlex::Reactive::Component); this controller binds DOM events to a single\n// HTTP round trip and lets Turbo apply the re-rendered component back in\n// (replace by default; method=\"morph\" — Response.morph — preserves focus).\n//\n// Wire format (client -> server), POST <action path>, turbo-stream Accept:\n// { token: \"<signed identity>\", act: \"<action>\", params: {...} } (JSON)\n// (`act`, not `action`: `action` is a reserved Rails routing param.)\n// The token is a MessageVerifier-signed { component, gid } — NO state is sent.\n// When the root holds a chosen <input type=\"file\">, the SAME payload is sent as\n// multipart FormData instead (token/act flat, params bracketed, files appended)\n// so an upload reaches the action (issue #34) — only the encoding differs.\n// The response is a <turbo-stream> that replaces the component by its id.\n//\n// Server -> client live updates use the SAME element id, pushed over the\n// stream transport (pgbus SSE / Action Cable) via the Streamable\n// .broadcast_* methods — so a click and a background broadcast converge on\n// one re-render unit.\n//\n// Custom turbo-stream action: the server tells the actor to full-navigate\n// (e.g. the record's slug changed and the current URL is now dead). It rides a\n// 200 turbo-stream — NOT an HTTP 3xx — so it never trips the response.redirected\n// bail below (which still correctly catches real auth/CSRF redirects). Registered\n// once on the Turbo global (no @hotwired/turbo import — the gem uses window.Turbo\n// everywhere, and a named import is unreliable under importmap/esbuild).\nexport function registerReactiveVisit() {\n const actions = window.Turbo?.StreamActions\n if (!actions || actions[\"reactive:visit\"]) return\n actions[\"reactive:visit\"] = function () {\n const url = this.getAttribute(\"data-url\")\n if (url) window.Turbo.visit(url, { action: \"advance\" })\n }\n}\n\n// Custom turbo-stream action: a TOKEN-ONLY refresh (issue #30). A partial\n// update (Response.streams / reply.streams) re-renders only PART of a component\n// — so there's no full-self replace to carry the next signed token. The server\n// instead emits `<turbo-stream action=\"reactive:token\" target=\"<id>\"\n// data-reactive-token-value=\"<fresh>\">`. #perform's #extractToken already reads\n// the token out of the response body for the NEXT queued request; this handler\n// keeps the DOM in sync too, writing the attribute onto the root element so the\n// `tokenValue` fallback stays fresh. It's a pure attribute set — no node is\n// replaced — so a focused <input> + caret survive (the whole point: update a\n// total cell without tearing down the field the user is typing in).\nexport function registerReactiveToken() {\n const actions = window.Turbo?.StreamActions\n if (!actions || actions[\"reactive:token\"]) return\n actions[\"reactive:token\"] = function () {\n const token = this.getAttribute(\"data-reactive-token-value\")\n const target = this.getAttribute(\"target\")\n if (!token || !target) return\n const el = document.getElementById(target)\n // Stimulus reads the token via the `token` value -> data-reactive-token-value.\n if (el) el.setAttribute(\"data-reactive-token-value\", token)\n }\n}\n\n// Custom turbo-stream action: SERVER-PUSHED client DOM ops (issue #97). The\n// server-side sibling of on_client's runOps — a reply (reply.<verb>.js(ops)) or\n// a broadcast (Streamable.broadcast_js_to) emits\n//\n// <turbo-stream action=\"reactive:js\" target=\"<optional root id>\"\n// data-reactive-ops=\"[[op, args], ...]\"></turbo-stream>\n//\n// and Turbo invokes this handler with `this` bound to that <turbo-stream>\n// element. It runs the ops through the SAME frozen CLIENT_OPS whitelist as\n// runOps (client-side default-deny — an unknown op warns + is skipped), so a\n// forged/stale ops attr can never break the page or execute anything off the\n// vocabulary. NO token, NO fetch — a pure local DOM mutation.\n//\n// `target` (optional, an element id) scopes op resolution to that root: \"@root\"\n// resolves to the target element itself and a selector resolves WITHIN it.\n// Without a target, ops resolve document-wide (a broadcast op like\n// add_class(\"#bell\", ...) that isn't anchored to one component). The op stream\n// is emitted AFTER all render streams in the reply (the endpoint appends it\n// last), so focus(\"[name=next]\") sees the freshly morphed DOM — Turbo applies\n// streams in document order.\nexport function registerReactiveJs() {\n const actions = window.Turbo?.StreamActions\n if (!actions || actions[\"reactive:js\"]) return\n actions[\"reactive:js\"] = function () {\n const list = parseOps(this.getAttribute(\"data-reactive-ops\"))\n if (!list.length) return\n const targetId = this.getAttribute(\"target\")\n // With a target: scope to that element (missing → no-op). Without: document.\n const root = targetId ? document.getElementById(targetId) : null\n if (targetId && !root) return\n applyOps(list, (args) => streamOpTargets(args, root))\n }\n}\n\n// --- Deferred reply segments (issue #165) ----------------------------------\n// The client half of reply.defer: the server's reply carries a\n// `<turbo-stream action=\"reactive:defer\" target=\"<id>\">` directive and the\n// real render reaches the SAME actor later — via a parallel fetch (pull) or a\n// pgbus one-shot stream (push). Everything here is MODULE-level, deliberately\n// OFF the per-controller request queue: the whole point is that the expensive\n// segment never blocks the actor's next action.\n//\n// Supersession is the correctness core: pendingDefers keys one in-flight\n// delivery per target id. A newer directive for the same target aborts the\n// older fetch (or removes the older stream source), and an arrival applies\n// ONLY while its entry is still current — so a fast typist's debounced\n// keystrokes can never paint stale totals over fresh ones.\nconst pendingDefers = new Map()\n\n// Test seam: clear the module-level registry between unit tests. Also resets\n// the one-shot settle-listener guard so a test's fresh document re-registers\n// the turbo:before-stream-render settler.\nexport function resetReactiveDefers() {\n pendingDefers.clear()\n deferStreamSettleRegistered = false\n}\n\n// Test seam: the `via` of a target's pending defer entry (or undefined) — lets\n// tests assert an entry was SETTLED (dropped) on arrival without exposing the\n// Map. Not used by the runtime.\nexport function pendingDeferVia(targetId) {\n return pendingDefers.get(targetId)?.via\n}\n\nlet deferStreamSettleRegistered = false\n\nexport function registerReactiveDefer() {\n const actions = window.Turbo?.StreamActions\n if (!actions || actions[\"reactive:defer\"]) return\n actions[\"reactive:defer\"] = function () {\n const target = this.getAttribute(\"target\")\n if (!target) return\n if (this.getAttribute(\"data-reactive-defer-via\") === \"stream\") {\n startStreamDefer(target, this)\n return\n }\n const token = this.getAttribute(\"data-reactive-defer-token\")\n if (!token) return\n startFetchDefer(target, token)\n }\n\n // Settle a STREAM-lane pendingDefers entry when its arrival lands: the job's\n // broadcast is a turbo-stream that replaces the target (and removes the\n // source). A document-level turbo:before-stream-render hook drops the Map\n // entry for a stream target the moment a stream renders against it — so the\n // entry never outlives the delivery (the fetch lane settles inline; the\n // stream lane's arrival is a broadcast this controller doesn't await, so it\n // needs this hook). Registered once; a no-op without document.\n if (!deferStreamSettleRegistered && typeof document !== \"undefined\" && document.addEventListener) {\n deferStreamSettleRegistered = true\n document.addEventListener(\"turbo:before-stream-render\", settleStreamDeferOnRender)\n }\n}\n\n// Drop a stream-lane pendingDefers entry when a turbo-stream renders against\n// its target id (the job's replace) OR removes its source element. Keyed by the\n// stream's target so an unrelated stream never settles a defer. Pure Map\n// cleanup — the DOM apply is Turbo's; this only releases our bookkeeping.\nfunction settleStreamDeferOnRender(event) {\n const streamEl = event.target\n const target = streamEl?.getAttribute?.(\"target\")\n if (!target) return\n // The arrival replaces #<target>; the source removal targets\n // reactive-defer-src-<target>. Either signals the stream delivered.\n const targetId = target.startsWith(\"reactive-defer-src-\")\n ? target.slice(\"reactive-defer-src-\".length)\n : target\n const entry = pendingDefers.get(targetId)\n if (entry?.via === \"stream\") pendingDefers.delete(targetId)\n}\n\n// The pull lane: mark the target pending and POST the signed defer token to\n// the defer endpoint, in parallel with everything else the page is doing.\nfunction startFetchDefer(targetId, token) {\n const el = document.getElementById(targetId)\n if (!el) {\n console.warn(`[phlex-reactive] reactive:defer target #${targetId} is not on the page — skipped`)\n return\n }\n supersedeDefer(targetId)\n markDeferPending(el)\n const entry = { via: \"fetch\", abort: new AbortController(), timedOut: false }\n pendingDefers.set(targetId, entry)\n performDeferFetch(targetId, entry, token)\n}\n\n// The push lane: subscribe a <pgbus-stream-source> to the server-signed\n// one-shot stream. Arrival + teardown need no client logic — the job's\n// broadcast carries the replace AND a remove of this source element (its\n// disconnectedCallback closes the SSE connection). since-id=0 on a fresh key\n// replays a broadcast that beat the subscription (the durable-lane guarantee).\nfunction startStreamDefer(targetId, directive) {\n const el = document.getElementById(targetId)\n if (!el) {\n console.warn(`[phlex-reactive] reactive:defer target #${targetId} is not on the page — skipped`)\n return\n }\n const src = directive.getAttribute(\"data-reactive-defer-src\")\n if (!src) return\n if (!globalThis.customElements?.get?.(\"pgbus-stream-source\")) {\n // The server chose push on server-side capability, but this page has no\n // pgbus client. Degrade to the fetch lane using the fallback token the push\n // directive carries — rather than dead-end the shimmer. No token (an app on\n // a bespoke transport) is a loud no-op.\n const fallbackToken = directive.getAttribute(\"data-reactive-defer-token\")\n if (fallbackToken) {\n startFetchDefer(targetId, fallbackToken)\n return\n }\n console.error(\n \"[phlex-reactive] reactive:defer via=stream but <pgbus-stream-source> is not registered \" +\n \"and no fallback token was provided — is the pgbus client loaded on this page?\",\n )\n return\n }\n supersedeDefer(targetId)\n markDeferPending(el)\n const source = document.createElement(\"pgbus-stream-source\")\n // Deterministic id: the JOB's broadcast removes it by this exact id — the\n // subscription tears itself down with the payload it delivered.\n source.id = deferSourceId(targetId)\n source.setAttribute(\"src\", src)\n source.setAttribute(\"since-id\", directive.getAttribute(\"data-reactive-defer-since-id\") ?? \"0\")\n source.setAttribute(\"hidden\", \"\")\n document.body.appendChild(source)\n // Record only { via } — NOT a strong ref to the source element. The job's\n // broadcast removes the source by its deterministic id (its\n // disconnectedCallback closes the SSE), so holding srcEl here would pin the\n // detached node in this module-level Map forever (a leak). Supersession\n // re-finds the element by id instead. The entry is dropped on\n // supersession or when the arriving broadcast replaces the target (a\n // turbo:before-stream-render hook, below).\n pendingDefers.set(targetId, { via: \"stream\" })\n}\n\n// The deterministic id of a target's one-shot <pgbus-stream-source>.\nfunction deferSourceId(targetId) {\n return `reactive-defer-src-${targetId}`\n}\n\nasync function performDeferFetch(targetId, entry, token) {\n // Bound the wait like the action fetch (issue #101) — a hung defer must not\n // shimmer forever. A manual timer (not AbortSignal.timeout) so the catch can\n // tell a TIMEOUT (fail loudly) from a SUPERSEDED abort (stay silent).\n const timer = setTimeout(() => {\n entry.timedOut = true\n entry.abort.abort()\n }, deferTimeoutMs())\n\n // The timeout is cleared ONLY after the body is fully read (below), not the\n // moment headers arrive — a server that streams headers then stalls the body\n // must still abort, or the shimmer hangs forever (the abort signal covers the\n // whole fetch + body read, mirroring #perform's AbortSignal.timeout).\n let response\n try {\n response = await fetch(deferPath(), {\n method: \"POST\",\n headers: {\n Accept: \"text/vnd.turbo-stream.html\",\n \"Content-Type\": \"application/json\",\n \"X-CSRF-Token\": deferCsrfToken(),\n },\n body: JSON.stringify({ token }),\n credentials: \"same-origin\",\n signal: entry.abort.signal,\n })\n } catch (error) {\n clearTimeout(timer)\n if (pendingDefers.get(targetId) !== entry) return // superseded — silent\n console.error(\"[phlex-reactive] deferred render failed\", error)\n failDefer(targetId, token)\n return\n }\n if (pendingDefers.get(targetId) !== entry) {\n clearTimeout(timer)\n return // superseded mid-flight\n }\n\n if (response.status === 204) {\n clearTimeout(timer)\n // render? false — keep the current content, just clear the pending state.\n settleDefer(targetId)\n return\n }\n if (!response.ok) {\n clearTimeout(timer)\n console.error(`[phlex-reactive] deferred render failed: HTTP ${response.status}`)\n failDefer(targetId, token, response.status)\n return\n }\n\n let html\n try {\n html = await response.text()\n } catch (error) {\n clearTimeout(timer)\n if (pendingDefers.get(targetId) !== entry) return\n console.error(\"[phlex-reactive] deferred render failed reading the body\", error)\n failDefer(targetId, token)\n return\n }\n clearTimeout(timer)\n if (pendingDefers.get(targetId) !== entry) return // superseded during read\n\n settleDefer(targetId)\n // A normal replace/morph of the target — the fresh root carries no pending\n // markers and a fresh action token, so the component lands interactive.\n window.Turbo.renderStreamMessage(html)\n}\n\n// Abort/unsubscribe whatever delivery is in flight for this target. The\n// deleted entry makes every late arrival fail its identity check — stale\n// content can never paint.\nfunction supersedeDefer(targetId) {\n const existing = pendingDefers.get(targetId)\n if (!existing) return\n pendingDefers.delete(targetId)\n if (existing.via === \"fetch\") existing.abort.abort()\n // Stream lane: re-find the old source by its deterministic id and remove it\n // (unsubscribe) — we deliberately don't hold a strong ref to the detached\n // node. Its disconnectedCallback closes the SSE.\n else document.getElementById(deferSourceId(targetId))?.remove?.()\n}\n\nfunction markDeferPending(el) {\n el.setAttribute(\"data-reactive-defer-pending\", \"true\")\n el.setAttribute(\"aria-busy\", \"true\")\n}\n\nfunction clearDeferPending(el) {\n el.removeAttribute(\"data-reactive-defer-pending\")\n el.removeAttribute(\"aria-busy\")\n}\n\n// Success/204: drop the registry entry, clear pending, and clear any prior\n// defer failure marker (recovery resets error-driven CSS, issue #100 style).\nfunction settleDefer(targetId) {\n pendingDefers.delete(targetId)\n const el = document.getElementById(targetId)\n if (!el) return\n clearDeferPending(el)\n el.removeAttribute(\"data-reactive-error\")\n}\n\n// Failure: clear pending (the shimmer must not lie), mark the root\n// (data-reactive-error=\"defer\" — style it in pure CSS), and emit a bubbling\n// reactive:error whose retry() re-enters the defer fetch with the SAME token\n// (still valid inside the TTL; an expired token 400s into this same path).\nfunction failDefer(targetId, token, status) {\n pendingDefers.delete(targetId)\n const el = document.getElementById(targetId)\n if (!el) return\n clearDeferPending(el)\n el.setAttribute(\"data-reactive-error\", \"defer\")\n const retry = () => {\n const fresh = document.getElementById(targetId)\n if (!fresh) {\n console.warn(\"[phlex-reactive] defer retry() ignored — the target left the DOM\")\n return\n }\n fresh.removeAttribute(\"data-reactive-error\")\n startFetchDefer(targetId, token)\n }\n el.dispatchEvent(\n new CustomEvent(\"reactive:error\", {\n bubbles: true,\n composed: true,\n detail: { kind: \"defer\", target: targetId, status, retry },\n }),\n )\n}\n\nfunction deferPath() {\n return document.querySelector('meta[name=\"phlex-reactive-defer-path\"]')?.content || \"/reactive/defer\"\n}\n\n// CSRF is read LIVE per request (Rails can rotate it) — same contract as the\n// controller's #csrfToken.\nfunction deferCsrfToken() {\n return document.querySelector('meta[name=\"csrf-token\"]')?.content ?? \"\"\n}\n\n// Same page-stable meta + default as the controller's #timeoutMs (issue #101),\n// parsed defensively so a typo'd meta can never disable the bound.\nfunction deferTimeoutMs() {\n const raw = document.querySelector('meta[name=\"phlex-reactive-timeout\"]')?.content\n const ms = Number(raw)\n return Number.isFinite(ms) && ms > 0 ? ms : 30000\n}\n\n// Document-level self-dismissing flashes (issue #100). A flash rendered with\n// dismiss_after: carries data-reactive-dismiss-after=\"<ms>\"; after the timeout\n// it removes itself. This is deliberately NOT a Stimulus controller — the flash\n// container is a plain host-app div (Response#flash appends into it) with no\n// controller attached, so nothing would honor the attr. A document-level scan\n// on turbo:before-stream-render (which fires for EVERY <turbo-stream> render —\n// a reply AND a broadcast) schedules removal for any newly-arrived dismissing\n// flash. Each is marked data-reactive-dismiss-scheduled so re-scans (a later\n// stream render) never double-schedule the same node. Registered once; the\n// guard flag makes a second call a no-op (bun imports the module once per run).\nlet dismissRegistered = false\nexport function registerReactiveDismiss() {\n if (dismissRegistered) return\n if (typeof document === \"undefined\" || !document.addEventListener) return\n dismissRegistered = true\n // turbo:before-stream-render fires BEFORE the stream is applied — and Turbo\n // then does `await nextRepaint(); await event.detail.render(this)`, so a bare\n // setTimeout(0) can run BEFORE the node is inserted (observed under Falcon).\n // WRAP event.detail.render instead: run Turbo's own render, then scan once it\n // has resolved — timing-independent and correct on every server. The event\n // fires for EVERY <turbo-stream> (a reply AND a broadcast), so both delivery\n // paths self-clean. detail.render may be absent on exotic streams — guard it.\n document.addEventListener(\"turbo:before-stream-render\", wrapStreamRenderForDismiss)\n}\n\n// Chain the dismissing-flash scan after Turbo's own stream render resolves, so\n// the scan sees the freshly-inserted node. Idempotent per event (marks\n// detail.render as already-wrapped) and defensive if detail/render is missing.\nfunction wrapStreamRenderForDismiss(event) {\n const detail = event.detail\n const original = detail?.render\n if (typeof original !== \"function\" || original.__reactiveDismissWrapped) {\n // No render to wrap (or already wrapped) — fall back to a post-repaint scan.\n if (typeof requestAnimationFrame === \"function\") requestAnimationFrame(scheduleReactiveDismissals)\n else setTimeout(scheduleReactiveDismissals, 0)\n return\n }\n const wrapped = async (streamElement) => {\n await original(streamElement)\n scheduleReactiveDismissals()\n }\n wrapped.__reactiveDismissWrapped = true\n detail.render = wrapped\n}\n\n// Scan for un-scheduled dismissing flashes and schedule each one's removal.\n// Kept a module function so the scan logic is testable and re-run on every\n// stream render.\nfunction scheduleReactiveDismissals() {\n const flashes = document.querySelectorAll(\"[data-reactive-dismiss-after]\")\n for (const el of flashes) {\n if (el.hasAttribute(\"data-reactive-dismiss-scheduled\")) continue\n const ms = Number(el.getAttribute(\"data-reactive-dismiss-after\"))\n if (!Number.isFinite(ms) || ms <= 0) continue\n el.setAttribute(\"data-reactive-dismiss-scheduled\", \"\")\n setTimeout(() => el.remove(), ms)\n }\n}\n\n// Test seam: reset the one-time registration guard so a fresh document stub in\n// the next test registers its own listener (bun runs all specs in one process).\nexport function __resetReactiveDismissForTest() {\n dismissRegistered = false\n}\n\n// Offline CSS hook (issue #101). Mirror data-reactive-offline on\n// document.documentElement from navigator.onLine, kept in sync by the window\n// online/offline events — so an app can dim a save button or show a banner with\n// PURE CSS and zero JS ([data-reactive-offline] .save { pointer-events: none }).\n// Guarded on window (needed for addEventListener AND navigator) so importing the\n// module in a non-browser (bun test) context is a no-op, and registered once\n// (the online/offline listeners are NOT {once}, so a second registerReactiveActions\n// call must not stack duplicates) — mirroring the dismiss guard + reset seam.\nlet offlineRegistered = false\nexport function registerReactiveOffline() {\n if (offlineRegistered) return\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n if (typeof window.addEventListener !== \"function\") return\n offlineRegistered = true\n // toggleAttribute(name, force) writes data-reactive-offline=\"\" (a bare boolean\n // attr the [data-reactive-offline] selector matches) or removes it — never the\n // \"true\" string. navigator.onLine === false is the reliable direction (a false\n // \"online\" is spec-permitted but rare, and this is only a presentational hook —\n // the authoritative offline signal is the #perform gate, not this attribute).\n // Fully defensive: a missing documentElement/toggleAttribute/navigator degrades\n // to a no-op — a presentational hook must NEVER throw during bootstrap.\n const sync = () => {\n const root = document.documentElement\n if (typeof root?.toggleAttribute !== \"function\") return\n root.toggleAttribute(\"data-reactive-offline\", globalThis.navigator?.onLine === false)\n }\n sync() // seed synchronously so first paint is correct\n window.addEventListener(\"online\", sync)\n window.addEventListener(\"offline\", sync)\n}\n\nexport function __resetReactiveOfflineForTest() {\n offlineRegistered = false\n}\n\n// Latency simulator dev aid (issue #102). On localhost the click→morph round\n// trip is ~5ms, so the pending/loading/optimistic affordances (aria-busy,\n// disable_with, busy_on, optimistic hints) flash by too fast to see while\n// developing or demoing them — the reason LiveView ships enableLatencySim(ms).\n//\n// enableLatencySim(ms) persists the delay to sessionStorage (session-scoped, so\n// it clears when the tab closes — never a config you forget you left on);\n// #perform reads it right before the fetch and awaits setTimeout(ms), stretching\n// the already-set busy window to something visible. disableLatencySim() clears\n// it. NAMED exports (the setConfirmResolver precedent) — but importmap module\n// exports are unreachable from the browser console, so registerReactiveActions\n// ALSO attaches these to window.PhlexReactive, and ONLY when the app opts in with\n// <meta name=\"phlex-reactive-env\" content=\"development\"> (see #attachLatencyHandle).\nexport const LATENCY_KEY = \"phlex-reactive:latency\"\n\n// One-time \"sim active\" banner guard (module-level, mirroring offlineRegistered):\n// #maybeSimulateLatency warns ONCE while the sim is on, not once per request.\nlet latencyBannerShown = false\n\nexport function enableLatencySim(ms) {\n if (typeof sessionStorage === \"undefined\") return\n sessionStorage.setItem(LATENCY_KEY, String(ms))\n}\n\nexport function disableLatencySim() {\n if (typeof sessionStorage === \"undefined\") return\n sessionStorage.removeItem(LATENCY_KEY)\n // Re-arm the one-time \"sim active\" banner: turning the sim OFF is the lifecycle\n // boundary, so a later enableLatencySim() in the same session re-announces that\n // the sim is on (otherwise the guard would stay set across an off→on cycle and\n // swallow the banner). Matches the __resetReactiveLatencyForTest seam.\n latencyBannerShown = false\n}\n\n// The dev gate. importmap module exports aren't reachable from the DevTools\n// console, so we expose the two functions on a window handle — but ONLY when the\n// app authored <meta name=\"phlex-reactive-env\" content=\"development\">. There is\n// NO engine-emitted meta (the engine can't inject into the host layout); the\n// install generator ships the snippet commented. Without the meta: no global\n// handle at all, and #perform short-circuits on the null sessionStorage read —\n// zero production surface. The `?.content` chain is fully defensive (a stubbed\n// document with no querySelector, a missing meta) so bootstrap never throws.\nfunction attachLatencyHandle() {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n const env = document.querySelector?.('meta[name=\"phlex-reactive-env\"]')?.content\n if (env !== \"development\") return\n window.PhlexReactive = { enableLatencySim, disableLatencySim }\n}\n\n// Test seam: forget the one-time active-sim banner so the next test re-warns.\nexport function __resetReactiveLatencyForTest() {\n latencyBannerShown = false\n}\n\nexport function registerReactiveActions() {\n registerReactiveVisit()\n registerReactiveToken()\n registerReactiveJs()\n registerReactiveDefer()\n registerReactiveDismiss()\n registerReactiveOffline()\n attachLatencyHandle()\n}\n\n// Escape a DOM id for safe interpolation into a RegExp (an id can legally contain\n// regex metacharacters like `.`/`:` — e.g. an `escape:`-namespaced or dotted id).\n// Used by #extractToken to match the stream that re-renders THIS element by id.\nexport function escapeRegExp(string) {\n return string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n\nif (typeof window !== \"undefined\") {\n if (window.Turbo) registerReactiveActions()\n else document.addEventListener(\"turbo:load\", registerReactiveActions, { once: true })\n}\n\n// --- Registration guard (issue #26 part 2) -------------------------------\n// In a `lazyLoadControllersFrom(\"controllers\", application)` app, only\n// controllers under app/javascript/controllers/ are registered. This module\n// lives outside that dir, so importing it isn't enough — `data-controller=\n// \"reactive\"` does NOTHING until the host runs application.register(\"reactive\",\n// ...). The failure is silent: components render, but no action ever fires.\n//\n// We can't warn from connect() in that case (connect never runs). Instead, once\n// the page is ready, if reactive elements exist but no controller has connected,\n// the controller wasn't registered — so we warn, pointing at the fix.\nlet reactiveConnected = false\n\nexport function checkReactiveRegistration() {\n if (reactiveConnected) return\n if (typeof document === \"undefined\") return\n const els = document.querySelectorAll('[data-controller~=\"reactive\"]')\n if (!els || els.length === 0) return\n console.warn(\n \"[phlex-reactive] found \" + els.length + ' element(s) with data-controller=\"reactive\" ' +\n \"but the reactive controller never connected. It is loaded but not registered — \" +\n 'add `application.register(\"reactive\", ReactiveController)` (importmap) or import it ' +\n \"into app/javascript/controllers/ for lazyLoadControllersFrom apps. See the README.\"\n )\n}\n\n// Test seams (no-ops in production usage).\nexport function __resetReactiveRegistrationForTest() {\n reactiveConnected = false\n}\nexport function __markReactiveConnectedForTest() {\n reactiveConnected = true\n}\n\nif (typeof window !== \"undefined\" && typeof document !== \"undefined\") {\n // Defer past initial controller connection (a microtask/tick after ready).\n const scheduleCheck = () => setTimeout(checkReactiveRegistration, 0)\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", scheduleCheck, { once: true })\n } else {\n scheduleCheck()\n }\n}\n\n// The interpret-time attribute-name allowlist (issue #96) — the SECOND half of\n// the two-sided default-deny. The Ruby builder already refuses these at build\n// time; this guards a hand-built / forged ops attr from bypassing it. Refused:\n// event handlers (on*, XSS), URL-bearing names (a javascript: navigation\n// surface), and style (CSS injection). Case-insensitive, mirroring js.rb.\nconst REFUSED_ATTR_URL = new Set([\"href\", \"src\", \"srcdoc\", \"action\", \"formaction\", \"xlink:href\", \"style\"])\nfunction attrRefused(name) {\n const lower = String(name).toLowerCase()\n return lower.startsWith(\"on\") || REFUSED_ATTR_URL.has(lower)\n}\n\n// Run an animated visibility change (issue #96 `transition:`). `flip` performs\n// the actual hidden-flag change; `[during, from, to]` are class lists applied\n// AROUND it. Cleanup (removing during+to) is awaited via `animationend` OR a\n// setTimeout fallback — whichever comes first — so an element with NO animation\n// never leaves the helper classes stuck (the op chain itself is not blocked:\n// cleanup is fire-and-forget, later ops run immediately). The fallback and the\n// listener share a one-shot `done` guard so cleanup runs exactly once.\nfunction runTransition(el, transition, flip) {\n const [during, from, to] = transition\n el.classList.add(during, from)\n flip()\n requestAnimationFrame(() => {\n el.classList.remove(from)\n el.classList.add(to)\n })\n\n let done = false\n const cleanup = () => {\n if (done) return\n done = true\n el.classList.remove(during, to)\n }\n el.addEventListener(\"animationend\", cleanup, { once: true })\n // ~10% over a common 300ms transition; also the ONLY path for a non-animated\n // element (animationend never fires there), so it must always be scheduled.\n setTimeout(cleanup, 350)\n}\n\n// The client-op whitelist behind on_client (issue #95, extended in #96). Mirrors\n// Phlex::Reactive::JS's vocabulary; an op name not in this map is\n// warn-and-skipped by #applyOps (client-side default-deny — a stale or newer\n// ops attr must never break the page). Each op is a pure, local DOM mutation:\n// nothing is read back, nothing is sent anywhere. Frozen so nothing can be\n// registered into it at runtime — extending the vocabulary is a gem change,\n// not an app hook.\nconst CLIENT_OPS = Object.freeze({\n show: (el, args) => setHidden(el, false, args),\n hide: (el, args) => setHidden(el, true, args),\n toggle: (el, args) => setHidden(el, !el.hidden, args),\n add_class: (el, args) => el.classList.add(...(args.classes ?? [])),\n remove_class: (el, args) => el.classList.remove(...(args.classes ?? [])),\n toggle_class: (el, args) => (args.classes ?? []).forEach((c) => el.classList.toggle(c)),\n\n // Attribute ops (issue #96), interpret-time allowlisted. set_attr writes the\n // (already-stringified) value; toggle_attr adds a missing attr (value \"\") or\n // removes a present one; remove_attr removes it. A refused name warns + skips.\n set_attr: (el, args) => {\n if (guardAttr(args.name)) el.setAttribute(args.name, args.value ?? \"\")\n },\n remove_attr: (el, args) => {\n if (guardAttr(args.name)) el.removeAttribute(args.name)\n },\n toggle_attr: (el, args) => {\n if (!guardAttr(args.name)) return\n if (el.hasAttribute(args.name)) el.removeAttribute(args.name)\n else el.setAttribute(args.name, \"\")\n },\n\n // Focus ops (issue #96). focus targets the match itself; focus_first targets\n // its first focusable descendant (opened-menu → first menuitem).\n focus: (el) => el.focus?.(),\n focus_first: (el) => firstFocusable(el)?.focus?.(),\n\n // Text op (issue #159): set textContent — XSS-safe by construction (never\n // innerHTML), strictly less powerful than set_attr. Change-guarded like\n // #mirrorText. With global: true it is the cross-root text escape: paint a\n // value into a recap node OUTSIDE the component's root.\n text: (el, args) => {\n const text = String(args.value ?? \"\")\n if (el.textContent !== text) el.textContent = text\n },\n\n // Dispatch a bubbling CustomEvent (issue #96). RAW element.dispatchEvent — the\n // controller SHADOWS Stimulus's this.dispatch helper, so it must not be used.\n dispatch: (el, args) => {\n el.dispatchEvent(new CustomEvent(args.name, { bubbles: true, composed: true, detail: args.detail ?? {} }))\n },\n})\n\n// Apply a hidden-flag change, optionally animated by a [during, from, to]\n// transition (issue #96). Split out so show/hide/toggle share it.\nfunction setHidden(el, hidden, args) {\n if (args?.transition) runTransition(el, args.transition, () => (el.hidden = hidden))\n else el.hidden = hidden\n}\n\n// The interpret-time attribute guard: refuse (warn + skip) a name off the\n// allowlist. Returns true when the op may proceed.\nfunction guardAttr(name) {\n if (!attrRefused(name)) return true\n console.warn(`[phlex-reactive] refused client attr op on ${JSON.stringify(name)} — skipped`)\n return false\n}\n\n// A cross-root mirror target must be a single ID selector (issue #159) — \"#\" +\n// a CSS identifier, nothing else. The client half of the two-sided default-deny\n// (reactive_compute's `mirror:` validates the SAME shape loudly at declare\n// time): a hand-built mirror attr must not widen a declared text mirror into a\n// page-wide selector write. A refused selector warns + skips (its siblings\n// still apply), matching the attr-allowlist posture.\nconst MIRROR_ID_SELECTOR = /^#[A-Za-z_][\\w-]*$/\nfunction guardMirrorSelector(selector) {\n if (typeof selector === \"string\" && MIRROR_ID_SELECTOR.test(selector)) return true\n console.warn(`[phlex-reactive] refused cross-root mirror target ${JSON.stringify(selector)} — skipped`)\n return false\n}\n\n// Evaluate a show binding's declared literal predicate (issue #161) against\n// the controlling field's current value. Exactly one of the three predicate\n// attrs decides: equals (value === literal), not (value !== literal), in\n// (value ∈ a JSON string list). The vocabulary is fixed and literal-only —\n// never an expression, so there is no eval surface (the reactive_show helper\n// enforces the same shape loudly at render; this is the client half of the\n// two-sided posture). Returns true/false for a decidable binding, or null for\n// a malformed/missing predicate — the caller SKIPS a null so a hand-built or\n// stale binding never flips visibility it doesn't understand (default-deny,\n// like the op whitelist).\nfunction showBindingMatches(el, value) {\n const equals = el.getAttribute(\"data-reactive-show-equals\")\n if (equals !== null) return value === equals\n const not = el.getAttribute(\"data-reactive-show-not\")\n if (not !== null) return value !== not\n const inRaw = el.getAttribute(\"data-reactive-show-in\")\n if (inRaw !== null) {\n try {\n const list = JSON.parse(inRaw)\n if (Array.isArray(list)) return list.includes(value)\n } catch {\n // fall through to the warn below — malformed JSON and a non-array both skip\n }\n console.warn(`[phlex-reactive] malformed reactive_show in: list ${JSON.stringify(inRaw)} — skipped`)\n return null\n }\n // Numeric threshold predicates (issue #176 part B): gte/gt/lte/lt read the\n // literal off its own flat attr and compare Number(value) against it. Any\n // present numeric attr decides the binding — a non-numeric field value (NaN)\n // is false (hidden), and a non-numeric LITERAL warn-skips (null).\n for (const key of SHOW_NUMERIC_KEYS) {\n const raw = el.getAttribute(`data-reactive-show-${key}`)\n if (raw !== null) return numericPredicateMatches(key, raw, value)\n }\n console.warn(\"[phlex-reactive] a reactive_show binding declares no predicate — skipped\")\n return null\n}\n\n// The numeric threshold keys (issue #176 part B) — the client half of the Ruby\n// SHOW_NUMERIC_KEYS. Order-independent; the evaluator reads the one that's\n// present. Each coerces BOTH sides to Number and compares.\nconst SHOW_NUMERIC_KEYS = [\"gte\", \"gt\", \"lte\", \"lt\"]\n\n// Evaluate one numeric threshold predicate against a field value. Returns\n// true/false for a decidable comparison, or null when the LITERAL itself is\n// non-numeric (a malformed binding — warn-skip, default-deny). A non-numeric\n// FIELD value (empty/blank/garbage) is treated as NaN → false: the\n// reveal-on-threshold notice stays hidden, the safe default. Shared by the\n// owned-binding evaluator (raw string literal off an attr) and the\n// cross-root/compound evaluator (a literal that arrived as a JSON number or\n// string).\nfunction numericPredicateMatches(key, literal, value) {\n const rhs = Number(literal)\n if (Number.isNaN(rhs)) {\n console.warn(`[phlex-reactive] reactive_show ${key}: needs a numeric literal, got ${JSON.stringify(literal)} — skipped`)\n return null\n }\n // A blank/whitespace field value must fail closed. Number(\"\") and\n // Number(\" \") are 0 (NOT NaN), so a bare Number()+isNaN check would wrongly\n // reveal a `lte:`/`lt:`/`gte: 0` binding on an EMPTY field. Force the\n // empty/blank case to NaN so the \"blank → hidden\" contract holds for every\n // operator, not just the ones where 0 happens to fail the comparison.\n const trimmed = value == null ? \"\" : String(value).trim()\n const n = trimmed === \"\" ? NaN : Number(trimmed)\n if (Number.isNaN(n)) return false\n switch (key) {\n case \"gte\":\n return n >= rhs\n case \"gt\":\n return n > rhs\n case \"lte\":\n return n <= rhs\n case \"lt\":\n return n < rhs\n default:\n return null\n }\n}\n\n// Evaluate an ALREADY-PARSED show predicate object (issue #164) — the\n// reactive_show_targets map embeds { equals/not/in } directly in its JSON, so\n// unlike showBindingMatches there are no attrs to read or re-parse. The same\n// literal-only vocabulary; anything else (empty, unknown keys, a non-array\n// in:) returns null and the caller warn-skips that target (default-deny — a\n// hand-built map entry must never flip visibility it doesn't declare).\nfunction showPredicateMatches(pred, value) {\n if (!pred || typeof pred !== \"object\") return null\n if (typeof pred.equals === \"string\") return value === pred.equals\n if (typeof pred.not === \"string\") return value !== pred.not\n if (Array.isArray(pred.in)) return pred.in.includes(value)\n // Numeric threshold predicates (issue #176 part B): the literal arrives as a\n // JSON number (or a numeric string) embedded in the predicate object — one\n // shared numericPredicateMatches with the owned-binding evaluator.\n for (const key of SHOW_NUMERIC_KEYS) {\n if (key in pred) return numericPredicateMatches(key, pred[key], value)\n }\n return null\n}\n\n// The selector matching every OWNED-element show binding: single-field\n// (data-reactive-show-field, issue #161) OR compound all:/any:\n// (data-reactive-show, issue #176). Both the connect() gate and the sync walk\n// use it so a compound-only root still enables the sync.\nconst SHOW_BINDING_SELECTOR = \"[data-reactive-show-field], [data-reactive-show]\"\n\n// Parse a compound show binding's JSON payload (issue #176 part A). Malformed\n// JSON degrades to null WITH a warn — a bad binding must never throw or blank\n// the page (client-side default-deny), but a collision (two bindings' JSON\n// mix-joined) is worth surfacing.\nfunction parseShowCompound(raw) {\n try {\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed\n } catch {\n // fall through to the warn\n }\n console.warn(`[phlex-reactive] malformed compound reactive_show payload ${JSON.stringify(raw)} — skipped`)\n return null\n}\n\n// Evaluate a COMPOUND show binding (issue #176 part A): a parsed\n// { all: [term, …] } or { any: [term, …] } payload, where each term is\n// { field, <predicate> }. `fieldValue(name)` resolves the OWNED field's current\n// value (memoized by the caller). all: ANDs every term, any: ORs them. A term\n// whose field is missing (null) or whose predicate is malformed/unknown folds\n// as FALSE — fail-closed (the issue's default-deny lean): a broken AND term can\n// never pass, a broken OR term can never reveal. Returns true/false for a\n// decidable payload, or null for a malformed payload (no all:/any: array) so\n// the caller warn-skips and leaves visibility alone.\nfunction compoundShowMatches(payload, fieldValue) {\n if (!payload || typeof payload !== \"object\") return null\n const connective = Array.isArray(payload.all) ? \"all\" : Array.isArray(payload.any) ? \"any\" : null\n if (!connective) return null\n const terms = payload[connective]\n if (terms.length === 0) return null // an empty compound decides nothing — skip\n\n const results = terms.map((term) => {\n if (!term || typeof term !== \"object\" || typeof term.field !== \"string\") return false\n const value = fieldValue(term.field)\n if (value === null) return false // no owned field with that name → fail-closed\n const match = showPredicateMatches(term, value)\n return match === true // null (malformed term) or false both fold to false\n })\n\n return connective === \"all\" ? results.every(Boolean) : results.some(Boolean)\n}\n\n// A cross-root show target must be a single ID selector (issue #164) — the\n// SAME shape the #159 mirror enforces (one shared regex), with its own warn so\n// a refused show target is distinguishable in the console. The client half of\n// the two-sided default-deny: reactive_show_targets raises at declare time; a\n// hand-built wire attr must not widen the escape to class/compound selectors.\n// A refused selector warns + skips — its siblings still apply.\nfunction guardShowTargetSelector(selector) {\n if (typeof selector === \"string\" && MIRROR_ID_SELECTOR.test(selector)) return true\n console.warn(`[phlex-reactive] refused cross-root show target ${JSON.stringify(selector)} — skipped`)\n return false\n}\n\n// The first focusable descendant of `el`, in document order — the natural\n// keyboard target inside an opened menu/dialog. Covers the standard focusable\n// set; :not([tabindex=\"-1\"]) drops explicitly-removed nodes. Returns null when\n// nothing inside is focusable (focus_first then no-ops).\nconst FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\nfunction firstFocusable(el) {\n return el.querySelectorAll?.(FOCUSABLE)?.[0] ?? null\n}\n\n// Parse a [[name, args], ...] op list from a raw attr/param. An array passes\n// through; a JSON string is parsed; anything malformed degrades to [] — a bad\n// ops attr must NEVER break the page (client-side default-deny). Shared by the\n// controller's runOps and the reactive:js stream action (issue #97).\nfunction parseOps(raw) {\n if (Array.isArray(raw)) return raw\n if (typeof raw !== \"string\") return []\n try {\n const list = JSON.parse(raw)\n return Array.isArray(list) ? list : []\n } catch {\n return []\n }\n}\n\n// Interpret a [[name, args], ...] op list against the frozen CLIENT_OPS\n// whitelist (issues #95/#96/#97). `resolveTargets(args)` returns the element(s)\n// an op applies to — the controller scopes to its root (excluding nested\n// reactive roots); the reactive:js stream action scopes to its target root (or\n// the document). An unknown name warns and is SKIPPED while the rest of the\n// chain still applies — client-side default-deny, one bad op never takes down\n// its siblings. Object.hasOwn (not a bare read) so inherited Object members\n// (\"constructor\") can't masquerade as ops.\nfunction applyOps(list, resolveTargets) {\n for (const entry of list) {\n if (!Array.isArray(entry)) continue\n const [name, args = {}] = entry\n if (!Object.hasOwn(CLIENT_OPS, name)) {\n console.warn(`[phlex-reactive] unknown client op ${JSON.stringify(name)} — skipped`)\n continue\n }\n for (const el of resolveTargets(args)) CLIENT_OPS[name](el, args)\n }\n}\n\n// Resolve a reactive:js op's targets against its `target` root (issue #97).\n// \"@root\" is the root element itself; a selector resolves WITHIN it; a bare\n// selector with no root (no `target` attr on the stream) resolves document-wide\n// — a broadcast op anchored by a global selector (#bell) rather than a\n// component. Unlike the controller path there is no nested-reactive-root\n// ownership filter: a server-pushed op names its own scope explicitly —\n// including `global: true`, which opts a single op out of the target-root\n// scope to document-wide resolution (issue #159; the same escape the builder\n// documents for the controller path).\nfunction streamOpTargets(args, root) {\n const to = args.to\n if (root) {\n if (to === \"@root\") return [root]\n if (typeof to !== \"string\" || to === \"\") return []\n if (args.global) return [...document.querySelectorAll(to)]\n return [...root.querySelectorAll(to)]\n }\n // No target root: document-scoped. \"@root\" is meaningless here (nothing to\n // anchor to) → no-op; a selector matches document-wide.\n if (typeof to !== \"string\" || to === \"\" || to === \"@root\") return []\n return [...document.querySelectorAll(to)]\n}\n\n// Register this controller eagerly (not lazily) so a click immediately after\n// page load is never missed. The phlex-reactive engine auto-pins it with\n// preload: true for importmap apps; see the README for esbuild/webpack.\nexport default class extends Controller {\n static values = {\n token: String, // signed identity token (component + record gid/state)\n }\n\n #tokenCache // freshest token, threaded synchronously across queued requests\n #debounceTimers = new Map() // trigger element -> { timer, flush } pending dispatch\n #throttleTimers = new Map() // trigger element -> Map(action -> suppression timer)\n #actionPathCache // page-stable action path, resolved once per controller\n #timeoutMsCache // page-stable request timeout (ms), resolved once per controller (issue #101)\n #tokenRegexCache // { id, token, self } — #extractToken's two per-id RegExps, rebuilt on id change (issue #118)\n // Loading-state bookkeeping (issue #99). All keyed so overlapping enqueues\n // refcount correctly and never clobber each other:\n #busyPending = 0 // root aria-busy pending counter (remove only at zero)\n #busyActions = new Map() // action -> in-flight count (root's space-separated busy set + busy_on)\n #busyTokenCounts = new WeakMap() // element -> Map(action -> count): its data-reactive-busy token set\n #loadingSnapshots = new Map() // trigger element -> { count, disabled, text } refcounted snapshot\n // Dirty tracking (issue #103): the bound re-scan (turbo:morph-element) and the\n // navigate-away guard handlers, held so disconnect() can remove exactly them.\n #boundScanDirty\n #boundBeforeUnload\n #boundBeforeVisit\n // Show bindings (issue #161): the ONE delegated sync handler shared by the\n // root's input/change/turbo:morph-element listeners, held for teardown.\n #boundSyncShow\n // Option filtering (issue #163): the ONE delegated sync handler shared by the\n // root's input/turbo:morph-element listeners, held for teardown.\n #boundSyncFilter\n // Lazy initial mount (issue #165): the bound re-probe attached to\n // turbo:morph-element so a Turbo page-refresh morph re-fires the defer fetch.\n #boundProbeLazyDefer\n\n // Mark that a reactive controller actually connected, so the registration\n // guard above knows the controller was registered (issue #26 part 2).\n connect() {\n reactiveConnected = true\n\n // Root-id guard (issue #48). The token round trip assumes the reactive root\n // element's id == component.id: the server targets component.id and the client\n // self-matches its NEXT token by this.element.id (#extractToken, issue #46).\n // If `id:` landed on a CHILD instead of the `**reactive_attrs` root, this id is\n // \"\" — #extractToken falls back to the FIRST token in the response (a child's),\n // so the next action POSTs a foreign token → endpoint default-deny → silent 403.\n // Warn NOW (on connect) so the failure surfaces on page load, not on click 2.\n if (this.element.id === \"\") {\n console.warn(\n \"[phlex-reactive] a reactive root has no id; its next-action token can't self-match \" +\n \"and may fall back to the first token in the response → a silent HTTP 403 on the NEXT action. \" +\n \"Put id: on the SAME element as reactive_attrs — use div(**reactive_root) (emits id + token together), \" +\n \"or div(id:, **reactive_attrs). The id: must NOT be on a child. See the README.\"\n )\n }\n\n // Lazy initial mount (issue #165): a reactive_lazy shell carries its defer\n // token as a ROOT attribute — enter the SAME module-level fetch path a\n // reply directive uses (supersession, pending markers, error handling\n // included). Probe on connect (a plain replace / cache restoration\n // re-connects) AND on turbo:morph-element: a Turbo page-refresh MORPH\n // re-shows the shell while keeping the element CONNECTED and firing no\n // Stimulus lifecycle, so a connect-only probe would leave the morphed-in\n // shell shimmering forever. The supersession registry makes a duplicate\n // probe a no-op (same target id), so re-probing is safe. The attribute\n // stays on the shell precisely so a re-appearance re-fires.\n // Only wire the morph re-probe for a root that IS a lazy shell (carries the\n // token) — a component that never uses reactive_lazy pays nothing (no\n // listener), matching the dirty-tracking / show-sync gating precedent.\n if (this.element.getAttribute?.(\"data-reactive-defer-token\")) {\n this.#probeLazyDefer()\n this.#boundProbeLazyDefer = () => this.#probeLazyDefer()\n this.element.addEventListener?.(\"turbo:morph-element\", this.#boundProbeLazyDefer)\n }\n\n // Dirty tracking (issue #103) — ONLY when this root opts in (track_dirty: or a\n // reactive_field(dirty:)), so a component that never uses it pays nothing (no\n // baseline scan, no morph listener on every broadcast). A plain (outerHTML)\n // replace re-connects the controller, so seed the baseline scan here — the root\n // reflects current-vs-default WITHOUT waiting for the first input. An in-place\n // morph / broadcast morph keeps the element CONNECTED and fires no Stimulus\n // lifecycle, so ALSO listen for turbo:morph-element on this.element to re-scan\n // after the morph writes fresh default* attributes (reactive:applied is NOT a\n // valid hook — it fires when streams are handed to Turbo, BEFORE the DOM\n // mutation). Both listeners are torn down in disconnect().\n if (this.#dirtyTrackingEnabled()) {\n this.#boundScanDirty = () => this.#scanDirty()\n this.element.addEventListener?.(\"turbo:morph-element\", this.#boundScanDirty)\n this.#scanDirty()\n\n // warn_unsaved: arm a navigate-away guard gated on a LIVE dirty-count read\n // (never a cached snapshot — the count is re-derived from the DOM each time).\n // beforeunload covers a real browser unload; turbo:before-visit covers a\n // Turbo in-app navigation (it does NOT fire on restoration visits — the\n // documented gap). Registered on window only when the marker is present.\n if (this.element.getAttribute?.(\"data-reactive-warn-unsaved\") === \"true\") {\n this.#armUnsavedGuard()\n }\n }\n\n // Show bindings (issue #161) — ONLY when this root owns one, so a component\n // without any pays a single probe (the dirty-tracking gate precedent). ONE\n // delegated listener pair on the root (input + change bubble from every\n // owned field — no per-field wiring, and a reactive_compute output write\n // dispatches a real input event, so computed values drive visibility too).\n // The connect sync seeds the initial state — a plain replace re-connects —\n // and turbo:morph-element re-syncs after an in-place morph (which keeps the\n // element connected, fires no Stimulus lifecycle, and may preserve a\n // user-edited field value the server's hidden attrs don't reflect).\n if (this.#showSyncEnabled()) {\n this.#boundSyncShow = () => this.#syncShow()\n this.element.addEventListener?.(\"input\", this.#boundSyncShow)\n this.element.addEventListener?.(\"change\", this.#boundSyncShow)\n this.element.addEventListener?.(\"turbo:morph-element\", this.#boundSyncShow)\n this.#syncShow()\n }\n\n // Option filtering (issue #163) — ONLY when the root declares the binding\n // (reactive_filter emits both attrs together), so a component without one\n // pays two attribute reads. ONE delegated input listener on the root — the\n // handler re-filters only for events from the NAMED input, so keystrokes in\n // unrelated fields on a wide form never pay a filter pass. The connect sync\n // seeds from the input's current value (a plain replace re-connects; back\n // navigation may restore typed text), and turbo:morph-element re-applies\n // after an in-place morph (which keeps the element connected, fires no\n // Stimulus lifecycle, and may preserve the user's typed query while the\n // server re-rendered every option visible).\n if (this.#filterEnabled()) {\n this.#boundSyncFilter = (event) => {\n if (event?.type === \"input\" && !this.#filterInputEvent(event)) return\n this.#syncFilter()\n }\n this.element.addEventListener?.(\"input\", this.#boundSyncFilter)\n this.element.addEventListener?.(\"turbo:morph-element\", this.#boundSyncFilter)\n this.#syncFilter()\n }\n }\n\n // Whether this root opts into dirty tracking (issue #103): track_dirty: puts the\n // trackDirty descriptor on the ROOT's data-action; a per-field reactive_field(\n // dirty:) puts it on a descendant field. Either turns tracking on. A quick\n // attribute read + one scoped query, evaluated once per connect (a cold path).\n #dirtyTrackingEnabled() {\n if ((this.element.getAttribute?.(\"data-action\") ?? \"\").includes(\"reactive#trackDirty\")) return true\n const nodes = this.element.querySelectorAll?.('[data-action*=\"reactive#trackDirty\"]') ?? []\n for (const el of nodes) if (this.#ownsField(el)) return true\n return false\n }\n\n // Tear down any pending debounce timers when the controller leaves the DOM\n // (Turbo morph/navigation removes the element). Otherwise a timer that hasn't\n // fired yet would later call #enqueue on a disconnected controller — a round\n // trip against a detached element / stale token (issue #17 follow-up).\n // Throttle suppression timers (issue #80) are torn down the same way — a\n // leading-edge timer holds no pending POST, but leaving it running would leak\n // it past the element's life.\n disconnect() {\n this.#clearAllDebounces()\n this.#clearAllThrottles()\n this.#teardownDirtyTracking()\n this.#teardownShowSync()\n this.#teardownFilterSync()\n if (this.#boundProbeLazyDefer) {\n this.element.removeEventListener?.(\"turbo:morph-element\", this.#boundProbeLazyDefer)\n }\n }\n\n // Lazy initial mount probe (issue #165): fetch the real content when THIS\n // root is a reactive_lazy shell that still carries its defer token AND the\n // pending marker. Gating on the pending marker is what makes a re-probe (a\n // Turbo morph re-showing the shell) fire while a re-probe of an already\n // RESOLVED root (real content, no token, no marker) is a no-op. The\n // module-level supersession registry dedupes a duplicate in-flight fetch for\n // the same id, so calling this on both connect and every morph is safe.\n #probeLazyDefer() {\n const el = this.element\n if (!el?.id) return\n const token = el.getAttribute?.(\"data-reactive-defer-token\")\n if (!token) return\n if (el.getAttribute?.(\"data-reactive-defer-pending\") !== \"true\") return\n startFetchDefer(el.id, token)\n }\n\n // Serialize requests per component. Each round trip rewrites the signed\n // token in the DOM (state lives in the token, not the client). If events\n // fire faster than round trips complete, concurrent requests would all read\n // the SAME stale token and clobber each other (last-write-wins). Chaining on\n // a per-controller promise makes each dispatch wait for the previous one, so\n // it always uses the freshest token.\n dispatch(event) {\n // `window` (renamed: never shadow the global) and `outside` are the event-\n // modifier params (issue #80). The client decides preventDefault behavior\n // from event.params — set by the Ruby on() — never by sniffing the\n // Stimulus descriptor.\n const { action, params, debounce, throttle, confirm, outside, window: windowBound, optimistic, loading } =\n event.params\n if (!action) return\n\n // Outside guard FIRST (issue #80): an outside: trigger only fires for\n // events whose target is OUTSIDE this component's ROOT (containment against\n // this.element — .contains includes the root itself). An event inside the\n // root must be a COMPLETE no-op — before preventDefault (the page's native\n // click behavior is untouched) and before the reactive:before-dispatch\n // lifecycle event (nothing to announce, nothing to veto).\n if (outside && this.element.contains(event.target)) return\n\n // The trigger is event.currentTarget — the element on(...) was spread onto —\n // NOT event.target (issue #99). A `<button><span>Save</span></button>` click\n // has target === the span, which carries no params and is the wrong element\n // to disable / swap text on. currentTarget is the bound element; fall back to\n // target for a directly-invoked/synthetic event. Captured now because\n // #proceed runs in a later microtask (after the confirm resolver), by which\n // point currentTarget is reset to null.\n const target = event.currentTarget ?? event.target\n\n // Stop native behavior (button submit / FORM NAVIGATION) HERE, synchronously\n // within the event dispatch — BEFORE the (possibly async) confirm gate below.\n // preventDefault() only works while the event is still being handled; once we\n // await the confirm resolver it's too late, and a `submit` trigger would\n // natively POST the form and navigate before the reactive round trip runs\n // (issue #11). For a `click` trigger there's no default to miss. This holds\n // for debounced triggers too — the round trip is deferred, but the native\n // default must still be prevented now. (Moved ahead of the confirm branch in\n // issue #55: an async resolver means we can't preventDefault after awaiting.)\n //\n // ONLY for element-bound triggers: a window-bound trigger (window:/outside:,\n // issue #80) hears EVERY matching event on the page — preventDefault-ing\n // those would kill every link click while a dropdown is mounted. The page's\n // native behavior proceeds alongside the reactive round trip.\n //\n // The `checked: :keep` optimistic hint (issue #98) OPTS OUT: for a click-bound\n // checkbox/radio the unconditional preventDefault is exactly what stops the\n // native flip from happening before the morph — so a bare checkbox click\n // (which has no form-navigation default to lose) skips it and flips now, and\n // the failure revert snaps it back. A `change`-bound trigger is unaffected —\n // `change` isn't cancelable, so preventDefault was already a no-op there.\n if (!windowBound && !this.#keepsNativeToggle(optimistic, target)) event.preventDefault()\n\n // No confirm message → proceed straight away (unchanged fast path).\n if (!confirm) return this.#proceed(target, action, params, debounce, throttle, optimistic, loading)\n\n // Confirmation gate (issue #52, made overridable + async in #55). A reactive\n // trigger can't use Hotwire's data-turbo-confirm — this controller preempts\n // the event — so a `confirm:` message routes through confirmResolver (default\n // window.confirm; an app can override it to reuse Turbo.config.forms.confirm).\n // The resolver may be sync or async; call it INSIDE the chain (via the leading\n // .then) so even a SYNCHRONOUS override throw rejects this promise instead of\n // escaping dispatch — a throwing dialog is treated as a cancel, like the user\n // dismissing it. The .catch is scoped to the resolver step (→ false = cancel),\n // so a dismissed/erroring dialog never surfaces as an unhandled rejection AND a\n // genuine bug inside #proceed is NOT silently swallowed. Enqueue ONLY on a\n // truthy resolution — nothing is enqueued, no timer scheduled, otherwise.\n Promise.resolve()\n .then(() => confirmResolver(confirm))\n .catch(() => false)\n .then((ok) => {\n if (ok) this.#proceed(target, action, params, debounce, throttle, optimistic, loading)\n })\n }\n\n // CLIENT-ONLY trigger entry point (issue #95) — the zero-round-trip sibling\n // of dispatch(). Wired by on_client: applies the declared op chain\n // (data-reactive-ops-param, built by Phlex::Reactive::JS) locally. NO token,\n // NO params, NO fetch, ever. Ops are ephemeral UI: any server re-render of\n // the component resets whatever they toggled (by design — a signed action\n // owns state that must survive re-renders).\n runOps(event) {\n const { ops, outside, window: windowBound } = event.params\n\n // Outside guard FIRST — identical semantics to dispatch() (issue #80): an\n // outside: trigger is a COMPLETE no-op for events inside this root, before\n // preventDefault and before any op runs.\n if (outside && this.element.contains(event.target)) return\n\n // Element-bound triggers preventDefault (a bare button inside a <form>\n // must not submit it); window-bound triggers (window:/outside:) never do —\n // they hear every matching event on the page, and preventDefault-ing those\n // would kill native clicks site-wide (issue #80 rationale).\n if (!windowBound) event.preventDefault()\n\n this.#applyOps(this.#parseOps(ops))\n }\n\n // Dirty tracking (issue #103). Wired by reactive_field(dirty: true) /\n // reactive_root(track_dirty: true): an `input` on an owned field runs a FULL\n // re-scan of every field this root owns. NO round trip, NO shipped state — the\n // baseline is the DOM's own defaultValue/defaultChecked/defaultSelected (the\n // last server render). A full pass (not a per-target toggle) is essential for\n // radio groups: the deselected radio flips to checked=false and fires no input\n // event, so only re-scanning everything keeps its flag honest.\n trackDirty() {\n this.#scanDirty()\n }\n\n // Client-side compute (data binding). Wired by reactive_compute: an `input`\n // trigger (input->reactive#recompute) runs a REGISTERED JS reducer over the\n // named input fields and writes the named output fields WITH NO ROUND TRIP —\n // the \"instant\" half of the new/unpersisted-record UX. If the field ALSO\n // carries on(...) (a persisted record, or a synced draft), that debounced POST\n // still fires and the server reply reconciles; recompute just paints first.\n //\n // Reads inputs/outputs/reducer from the root's data-reactive-compute-* attrs\n // (set once by reactive_compute_attrs). A missing/unregistered reducer is a\n // no-op — a page must never break because a binding wasn't wired up.\n //\n // The reducer gets a second argument, meta = { changed } (issue #75): the\n // name of the declared input the triggering event edited, or null for a\n // direct call / an unowned or undeclared target. A multi-way rebalance\n // branches on it (edit c → derive a; else → derive c). Note that a #76\n // output write dispatches a real input event, so recompute RE-ENTERS with\n // changed = that output's name — the reducer must be convergent (see\n // compute.js) so the change guard settles the chain.\n recompute(event) {\n // Inputs may be a JSON ARRAY of names (array form — every input coerced\n // through Number, the shipped behavior) or a JSON OBJECT of name→type (hash\n // form, issue #104 — :number coerced, :string read raw). #parseComputeInputs\n // returns [name, type] pairs either way (array form defaults type \"number\").\n const inputPairs = this.#parseComputeInputs()\n const inputs = inputPairs.map(([name]) => name)\n\n // Resolve every declared input AND output through ONE per-call resolver whose\n // ownership probe is computed ONCE (issue #117), replacing the per-name\n // closest() walk #ownedField did on every read — a 30-field calculator paid\n // ~60 closest() sweeps per keystroke. #ownershipFilter returns a constant-true\n // predicate in the common no-nested-root case (skipping closest() entirely)\n // and the exact #ownsField check when a nested reactive root is present\n // (issue #15 scoping, byte-identical to before). Resolution is memoized in a\n // per-CALL Map, FIRST-WINS, so a name read as an input AND written as an\n // output resolves to the SAME element and is queried once.\n //\n // Why per-name `[name=\"X\"]` queries and not one bare `[name]` sweep: a single\n // sweep is the natural \"one walk\", but the resolver must issue the SAME\n // per-name query shape the field walk always has (the issue-#15 unit fakes\n // answer only `[name=\"X\"]`). It is O(distinct declared names) queries, not the\n // old O(inputs + outputs) — the ownership decision is hoisted out of the loop.\n // The memo is per-CALL only: an output write dispatches `input` (issue #76),\n // re-entering recompute, which correctly rebuilds a fresh map (a morph may\n // have replaced the nodes) — it is NEVER stored on the instance.\n const owns = this.#ownershipFilter()\n const byName = new Map()\n const ownedField = (name) => {\n if (byName.has(name)) return byName.get(name)\n let found = null\n for (const el of this.element.querySelectorAll(`[name=\"${name}\"]`)) {\n if (owns(el)) {\n found = el // FIRST-WINS (radio groups, Rails hidden+checkbox name pairs)\n break\n }\n }\n byName.set(name, found)\n return found\n }\n\n // Identity-mirror pass (issue #104), ALWAYS run — even with NO registered\n // reducer, so reactive_text(:title) mirrors a field into its text node with\n // zero reducer wiring. Each declared input's RAW string value is written to\n // its owned [data-reactive-text=\"<name>\"] node(s). It runs BEFORE the reducer\n // early-return below so a reducer-less binding still mirrors.\n for (const name of inputs) this.#mirrorText(name, ownedField(name)?.value ?? \"\")\n\n const key = this.element.getAttribute(\"data-reactive-compute-reducer-param\")\n const reduce = key ? computeReducer(key) : null\n if (!reduce) {\n // No reducer registered: the identity pass above still ran, so declared\n // cross-root mirrors of the INPUT names still paint (issue #159) — a\n // reducer-less binding mirrors, exactly like the owned-text-node case.\n this.#applyComputeMirrors({}, ownedField)\n return\n }\n\n const outputs = this.#parseComputeList(\"data-reactive-compute-outputs-param\")\n\n // Coerce each input per its declared type (issue #104): \"string\" → the raw\n // display string (blank/absent → \"\"); else (\"number\", the array-form default)\n // → the numeric coercion (blank/NaN → 0, the nanToZero the hand-written\n // calculators use). Reads from the memoized resolver — no re-query.\n const values = {}\n for (const [name, type] of inputPairs) {\n const field = ownedField(name)\n if (type === \"string\") {\n values[name] = field?.value ?? \"\"\n } else {\n const n = Number(field?.value)\n values[name] = Number.isFinite(n) ? n : 0\n }\n }\n\n // meta.changed stays on #changedComputeField (its own #ownsField check over\n // the raw event target) — NOT this resolver. The issue-#15 nested-rejection\n // test depends on that path being unchanged.\n const result = reduce(values, { changed: this.#changedComputeField(event, inputs) }) || {}\n for (const name of outputs) {\n if (!(name in result)) continue\n const field = ownedField(name)\n // Output resolution (issue #104): write to the owned named FIELD if one\n // exists, ELSE mirror to every owned [data-reactive-text=\"<name>\"] node.\n if (field) {\n // Real browsers do NOT fire `input` on a programmatic .value write (issue\n // #76), so after writing we dispatch a bubbling `input` ourselves — that's\n // what drives a chained repaint (a summary listener, a second compute),\n // matching the server's set_value + dispatch(\"input\") contract. The write\n // is CHANGE-GUARDED: an unchanged value is skipped entirely (no write, no\n // event). The guard is what lets a reducer with overlapping inputs/outputs\n // (the shipped payment_split shape) settle — an unconditional dispatch\n // would re-enter input->reactive#recompute forever.\n if (String(result[name]) === field.value) continue\n field.value = result[name]\n field.dispatchEvent(new Event(\"input\", { bubbles: true }))\n } else {\n // A text-node output: textContent, XSS-safe by construction. Change-\n // guarded too (compare before writing), but NO input dispatch — a text\n // node has no listener contract, so nothing chains off it.\n this.#mirrorText(name, result[name])\n }\n }\n\n // Cross-root text mirrors (issue #159) — AFTER the outputs are applied, so\n // a mirror keyed on a just-written output paints the settled value.\n this.#applyComputeMirrors(result, ownedField)\n }\n\n // Client-side list navigation (combobox keyboard nav, issue #72). Wired by\n // on(:search, …, listnav: \"[role=option]\"), which appends keyboard filters to\n // the input's data-action (keydown.down/up/enter/esc->reactive#listnav*) and\n // sets data-reactive-listnav-option-param. Arrow keys move a highlight among\n // the options WITH NO ROUND TRIP; Enter picks the highlighted option by\n // CLICKING IT (so its own on(:select) reactive trigger fires — selection stays\n // a signed action); Escape clears. Ephemeral highlight state lives on the DOM\n // (data-reactive-highlighted), never shipped to the client as trusted state.\n listnavNext(event) {\n this.#moveHighlight(event, +1)\n }\n\n listnavPrev(event) {\n this.#moveHighlight(event, -1)\n }\n\n // Enter: activate the highlighted option (fires its reactive select). No-op if\n // nothing is highlighted, and in that case DON'T preventDefault — Enter falls\n // through (there's no selection to make).\n listnavPick(event) {\n const options = this.#listnavOptions(event)\n const current = options.findIndex((el) => el.hasAttribute(\"data-reactive-highlighted\"))\n if (current < 0) return\n event.preventDefault()\n options[current].click()\n }\n\n listnavClose(event) {\n for (const el of this.#listnavOptions(event)) el.removeAttribute(\"data-reactive-highlighted\")\n }\n\n // Move the highlight by `step` (with wrap-around) among THIS root's options.\n // preventDefault stops Arrow keys from moving the caret in the search input.\n #moveHighlight(event, step) {\n const options = this.#listnavOptions(event)\n if (!options.length) return\n event.preventDefault()\n\n const current = options.findIndex((el) => el.hasAttribute(\"data-reactive-highlighted\"))\n // From nothing: Down highlights the first option, Up the last.\n const next = current < 0 ? (step > 0 ? 0 : options.length - 1) : (current + step + options.length) % options.length\n\n for (const el of options) el.removeAttribute(\"data-reactive-highlighted\")\n const chosen = options[next]\n chosen.setAttribute(\"data-reactive-highlighted\", \"true\")\n chosen.scrollIntoView?.({ block: \"nearest\" })\n }\n\n // The option elements this root owns (skips nested reactive roots, issue #15),\n // per the selector on data-reactive-listnav-option-param. The attr rides on the\n // TRIGGER element (the search input on(...) is spread onto), read from the\n // event; the options are still scoped to this controller's root. Empty when\n // unset. Falls back to the root for a directly-invoked call (unit tests). The\n // ownership predicate is hoisted ONCE per keypress (issue #117) — in the common\n // no-nested-root case it is a constant true, skipping a closest() walk per\n // option. Hidden options are excluded (issue #163): a reactive_filter (or any\n // `hidden` toggle) removes a row from the keyboard path too, so an Arrow can't\n // highlight — and Enter can't pick — an invisible option.\n #listnavOptions(event) {\n const trigger = event?.currentTarget ?? event?.target ?? this.element\n const selector =\n trigger.getAttribute?.(\"data-reactive-listnav-option-param\") ??\n this.element.getAttribute(\"data-reactive-listnav-option-param\")\n if (!selector) return []\n const owns = this.#ownershipFilter()\n return Array.from(this.element.querySelectorAll(selector)).filter((el) => !el.hidden && owns(el))\n }\n\n // Parse a JSON string list from a root data attr; [] on absence/parse error so\n // a malformed binding degrades to \"no fields\" rather than throwing on input.\n #parseComputeList(attr) {\n const raw = this.element.getAttribute(attr)\n if (!raw) return []\n try {\n const list = JSON.parse(raw)\n return Array.isArray(list) ? list : []\n } catch {\n return []\n }\n }\n\n // Parse the inputs param into [name, type] pairs (issue #104). The wire is a\n // JSON ARRAY of names (array form → every input typed \"number\", the shipped\n // numeric coercion) OR a JSON OBJECT of name→type (hash form → \":string\" read\n // raw, \":number\" coerced). Malformed/absent degrades to [] — a bad binding\n // must never throw on input.\n #parseComputeInputs() {\n const raw = this.element.getAttribute(\"data-reactive-compute-inputs-param\")\n if (!raw) return []\n try {\n const parsed = JSON.parse(raw)\n if (Array.isArray(parsed)) return parsed.map((name) => [name, \"number\"])\n if (parsed && typeof parsed === \"object\") return Object.entries(parsed)\n return []\n } catch {\n return []\n }\n }\n\n // The declared compute input the event just edited — the reducer's\n // meta.changed (issue #75). The triggering field counts only when it is a\n // named form control OWNED by this root (not a nested reactive root's, issue\n // #15) AND its name is among the declared compute inputs; anything else\n // (a direct call, an unowned/undeclared target) yields null.\n #changedComputeField(event, inputs) {\n const target = event?.target\n if (!target?.name || typeof target.closest !== \"function\") return null\n if (!inputs.includes(target.name)) return null\n return this.#ownsField(target) ? target.name : null\n }\n\n // Write `value` into every owned [data-reactive-text=\"<name>\"] node via\n // textContent (issue #104) — XSS-safe by construction (never innerHTML). Drives\n // both the identity mirror (an input's raw value) and a text-node output (a\n // reducer result with no matching field). Change-guarded (skip an unchanged\n // node) and NO input dispatch — a text node has no listener contract. String()\n // so a numeric result renders like the DOM would.\n #mirrorText(name, value) {\n const text = String(value)\n for (const node of this.#ownedTextNodes(name)) {\n if (node.textContent === text) continue\n node.textContent = text\n }\n }\n\n // Every [data-reactive-text=\"<name>\"] mirror OWNED by this root (skips nested\n // reactive roots, issue #15). Empty when none — reactive_text is optional.\n #ownedTextNodes(name) {\n const nodes = this.element.querySelectorAll(`[data-reactive-text=\"${name}\"]`)\n return Array.from(nodes).filter((el) => this.#ownsField(el))\n }\n\n // Cross-root text mirrors (issue #159): paint every DECLARED mirror name into\n // its allowlisted document-wide id targets via textContent — the opt-in escape\n // from root isolation (issue #15) for a recap OUTSIDE the computing root. The\n // value is the reducer's result when it produced one, else the owned field's\n // CURRENT value (an input identity mirror / a just-written output) — one\n // declaration covers all three shapes. A name with NO value this pass is\n // SKIPPED (a mirror never blanks a recap the reducer didn't feed). textContent\n // only (never innerHTML), change-guarded, and NO input dispatch — same\n // contract as #mirrorText. With no mirror declared this is one getAttribute\n // and out — the shipped compute path never touches the document.\n #applyComputeMirrors(result, ownedField) {\n const mirror = this.#parseComputeMirror()\n for (const [name, selectors] of Object.entries(mirror)) {\n const value = name in result ? result[name] : ownedField(name)?.value\n if (value === undefined || value === null) continue\n const text = String(value)\n for (const sel of Array.isArray(selectors) ? selectors : [selectors]) {\n if (!guardMirrorSelector(sel)) continue\n for (const node of document.querySelectorAll(sel)) {\n if (node.textContent === text) continue\n node.textContent = text\n }\n }\n }\n }\n\n // The declared cross-root mirror map (issue #159): a JSON object of\n // { name: [id selectors] } from data-reactive-compute-mirror-param (emitted by\n // reactive_compute's `mirror:`). Absent/malformed degrades to {} — a bad\n // binding must never throw on input.\n #parseComputeMirror() {\n const raw = this.element.getAttribute(\"data-reactive-compute-mirror-param\")\n if (!raw) return {}\n try {\n const parsed = JSON.parse(raw)\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? parsed : {}\n } catch {\n return {}\n }\n }\n\n // Enqueue the action — debounced if a debounce window is set, else immediately.\n // Split out of dispatch so both the no-confirm fast path and the post-confirm\n // microtask share one place (issue #55). `target` is captured up front because\n // this can run in a later microtask, after event.target has been reset.\n #proceed(target, action, params, debounce, throttle, optimistic, loading) {\n // Lifecycle veto point (issue #79): one cancelable reactive:before-dispatch\n // per user gesture — post-preventDefault, post-confirm, PRE-debounce (and\n // PRE-throttle, the same timing). event.preventDefault() skips the\n // debounce/throttle AND the enqueue entirely (nothing is scheduled).\n // retry() re-enters the queue directly, so this does NOT refire on a retry.\n // detail.params are the trigger's explicit params; sibling fields are\n // collected later, at send time.\n const before = this.#emit(\"reactive:before-dispatch\", {\n action,\n params: this.#parseParams(params),\n element: this.element,\n }, { cancelable: true })\n if (before.defaultPrevented) return\n\n // Debounced trigger (e.g. on(:update, event: \"input\", debounce: 300)):\n // coalesce rapid events into ONE round trip after a quiet period, instead of\n // one POST per keystroke (issue #17). A blur flushes a pending dispatch.\n // The optimistic hint (issue #98) and the loading state (issue #99) ride to\n // the flush too, so they apply ONCE per enqueue — a debounced input must not\n // flap toggle_class per keystroke, and its element must NOT be disabled\n // during the quiet period (that would break typing). Both apply at ENQUEUE.\n const ms = Number(debounce) || 0\n if (ms > 0) return this.#debounceDispatch(target, ms, action, params, optimistic, loading)\n\n // Throttled trigger (e.g. on(:track, event: \"scroll\", window: true,\n // throttle: 250), issue #80): LEADING-EDGE rate limit — fire the first\n // event immediately, drop the rest until the window elapses. debounce and\n // throttle are mutually exclusive (the Ruby on() raises on both).\n const throttleMs = Number(throttle) || 0\n if (throttleMs > 0) return this.#throttleDispatch(target, throttleMs, action, params, optimistic, loading)\n\n return this.#enqueue(action, params, optimistic, target, loading)\n }\n\n // Apply the optimistic hint ONCE (recording its inverse) and chain the round\n // trip, threading that inverse onto THIS queued request so the serialized\n // per-controller queue reverts the RIGHT request's hint on failure (issue\n // #98). Applying here — the single flush/enqueue point every path funnels\n // through — is what makes a hint apply once per enqueue, not per raw dispatch.\n //\n // The loading state (issue #99) applies here too, for the same reason: enqueue\n // is the moment the request is committed to the queue, so the always-on busy\n // vocabulary (data-reactive-busy on the trigger + root, aria-busy via a pending\n // counter, busy_on scoping) and the loading hint (disable + class + text swap)\n // cover the WHOLE pending window — queue wait included — not just the fetch. It\n // returns a `settle` closure that #perform runs in its finally (success OR\n // failure), guarded so a morph-replaced trigger is never clobbered.\n #enqueue(action, params, optimistic, target, loading) {\n const inverse = this.#applyOptimistic(optimistic, target)\n const settle = this.#applyLoading(action, target, loading)\n this.queue = (this.queue ?? Promise.resolve()).then(() => this.#perform(action, params, inverse, settle))\n return this.queue\n }\n\n // Reset a per-element timer; only enqueue the round trip after `ms` of quiet.\n // Also flush immediately on blur so leaving the field never drops the last\n // edit (a long debounce shouldn't swallow a value the user tabbed away from).\n #debounceDispatch(target, ms, action, params, optimistic, loading) {\n this.#clearDebounce(target)\n\n const flush = () => {\n this.#clearDebounce(target)\n this.#enqueue(action, params, optimistic, target, loading)\n }\n const timer = setTimeout(flush, ms)\n target?.addEventListener?.(\"blur\", flush, { once: true })\n this.#debounceTimers.set(target, { timer, flush })\n }\n\n #clearDebounce(target) {\n const pending = this.#debounceTimers.get(target)\n if (!pending) return\n clearTimeout(pending.timer)\n target?.removeEventListener?.(\"blur\", pending.flush)\n this.#debounceTimers.delete(target)\n }\n\n // Clear every pending debounce timer (used on disconnect). Reuses\n // #clearDebounce so all timer/listener teardown stays in one place. Snapshot\n // the keys first — #clearDebounce mutates the map as it goes.\n #clearAllDebounces() {\n for (const target of [...this.#debounceTimers.keys()]) this.#clearDebounce(target)\n }\n\n // Leading-edge throttle (issue #80), mirroring #debounceDispatch: the FIRST\n // event fires immediately; a suppression timer then drops further events\n // until the window elapses (no trailing fire — dropped, not queued). Timers\n // are keyed on action + target, NOT target alone: window-bound scroll/resize\n // events all share event.target === document, so two window-bound triggers\n // on one component would otherwise collide on one timer.\n #throttleDispatch(target, ms, action, params, optimistic, loading) {\n const timers = this.#throttleTimers.get(target) ?? new Map()\n if (timers.has(action)) return // inside the window — suppress\n\n const timer = setTimeout(() => {\n timers.delete(action)\n if (timers.size === 0) this.#throttleTimers.delete(target)\n }, ms)\n timers.set(action, timer)\n this.#throttleTimers.set(target, timers)\n return this.#enqueue(action, params, optimistic, target, loading) // leading edge: fire NOW\n }\n\n // Clear every throttle suppression timer (used on disconnect, alongside\n // #clearAllDebounces) so nothing outlives the element.\n #clearAllThrottles() {\n for (const timers of this.#throttleTimers.values()) {\n for (const timer of timers.values()) clearTimeout(timer)\n }\n this.#throttleTimers.clear()\n }\n\n // Raw-dispatch a lifecycle CustomEvent (issue #79). Deliberately NOT\n // Stimulus's this.dispatch() helper — that name is SHADOWED by this\n // controller's own dispatch(event) action method. Bubbling + composed so a\n // page-level listener (or `data-action=\"reactive:error->toast#show\"` on an\n // ancestor) hears it. After a plain (non-morph) replace this.element is a\n // DETACHED node — a bubbling event on it never reaches document listeners —\n // so fall back to dispatching on document itself.\n #emit(name, detail, { cancelable = false } = {}) {\n const event = new CustomEvent(name, { bubbles: true, composed: true, cancelable, detail })\n const root = this.element.isConnected ? this.element : document\n root.dispatchEvent(event)\n return event\n }\n\n // reactive:error detail: { action, params, kind, status?, body?, retry }.\n // `params` are the FULL params that were sent (collected fields + explicit\n // trigger params). retry() re-enters the request queue with the ORIGINAL raw\n // trigger params, so #perform re-reads the freshest token and RE-COLLECTS the\n // sibling fields — nothing stale is replayed. It does not refire\n // reactive:before-dispatch (one veto per user gesture), and it no-ops with a\n // warning once the root has left the DOM (retrying against a detached\n // element would post a stale token into nowhere).\n #emitError(action, rawParams, sentParams, extra) {\n const retry = () => {\n if (!this.element.isConnected) {\n console.warn(\"[phlex-reactive] retry() ignored — the reactive root left the DOM\")\n return\n }\n return this.#enqueue(action, rawParams)\n }\n this.#emit(\"reactive:error\", { action, params: sentParams, ...extra, retry })\n }\n\n // Mark the reactive root as errored (issue #100) with the failure kind, so an\n // app can style it purely in CSS ([data-reactive-error] { … }) with zero JS.\n // Guarded — a plain replace may have detached the root before the failure lands.\n #markError(kind) {\n if (this.element?.isConnected === false) return\n this.element?.setAttribute?.(\"data-reactive-error\", kind)\n }\n\n // Clear the failure marker on the next successful apply (issue #100).\n #clearError() {\n this.element?.removeAttribute?.(\"data-reactive-error\")\n }\n\n // Offline fallback (issue #100): a network failure reached no server, so there\n // is no body to render. Clone the content of a server-rendered\n // <template data-reactive-error-flash> into the flash region so the user still\n // sees SOMETHING. The template is app-authored (trusted) — this is a pure\n // deep clone, never client templating of untrusted data. No template, or no\n // flash container, is a silent no-op (a page without the opt-in is unchanged).\n #renderNetworkFallback() {\n const template = document.querySelector(\"[data-reactive-error-flash]\")\n if (!template?.content) return\n // The flash region is the host-app container Response#flash targets. Its id\n // defaults to \"flash\" (Phlex::Reactive.flash_target); an app that customized\n // it can point the fallback at the same node by putting the template's\n // data-reactive-error-flash value there — but the common case is #flash.\n const targetId = template.getAttribute(\"data-reactive-error-flash\") || \"flash\"\n const region = document.getElementById(targetId)\n if (!region) return\n region.appendChild(template.content.cloneNode(true))\n }\n\n // Latency simulator (issue #102): if enableLatencySim(ms) stored a delay in\n // sessionStorage, await it before the fetch so the busy window (already open\n // since enqueue) is actually visible on localhost. Reads the key LIVE per\n // request — like the CSRF token — so toggling the sim mid-session takes effect\n // on the very next action without a reload. A missing sessionStorage, an absent\n // key, or a non-positive/NaN value resolves immediately (no timer, no delay) —\n // the whole feature is inert for any app that never opts in. Warns ONCE while\n // active (module-level guard), not once per request.\n #maybeSimulateLatency() {\n if (typeof sessionStorage === \"undefined\") return Promise.resolve()\n const ms = Number(sessionStorage.getItem(LATENCY_KEY))\n if (!Number.isFinite(ms) || ms <= 0) return Promise.resolve()\n if (!latencyBannerShown) {\n latencyBannerShown = true\n console.warn(\n `[phlex-reactive] latency simulator ACTIVE — every action is delayed by ${ms}ms. ` +\n \"Call PhlexReactive.disableLatencySim() (or clear sessionStorage) to turn it off.\",\n )\n }\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n\n // Client debug mode (issue #108) — the \"devtools-lite\" lens. On when the Ruby\n // reactive_attrs stamped data-reactive-debug=\"true\" (Phlex::Reactive.debug).\n // Read live (a single getAttribute) so the whole feature is inert for any app\n // that never opts in: OFF → this returns false and #perform builds no debug\n // object, parses no response, and logs nothing (the \"zero cost when off\"\n // invariant — one nil-check per dispatch). Guarded for a stub root with no\n // getAttribute (unit harnesses) so it degrades to off, never throwing.\n #debugEnabled() {\n return this.element?.getAttribute?.(\"data-reactive-debug\") === \"true\"\n }\n\n // A monotonic timestamp for the round-trip duration (ms). performance.now is\n // monotonic (immune to a wall-clock adjustment mid-request); Date.now is the\n // fallback for an exotic environment without it. ONLY called on the debug path,\n // so it costs nothing when debug is off.\n #debugNow() {\n return typeof performance !== \"undefined\" && typeof performance.now === \"function\"\n ? performance.now()\n : Date.now()\n }\n\n // Parse a turbo-stream response's action + target pairs for the debug trace,\n // from the body text #perform ALREADY read (never a re-fetch). NAMES only — the\n // <template> contents (rendered HTML, the fresh token) are deliberately not\n // touched. A non-turbo-stream / empty body yields [] (nothing to report).\n #debugStreams(body) {\n if (!body) return []\n const streams = []\n const re = /<turbo-stream\\b([^>]*)>/g\n let match\n while ((match = re.exec(body)) !== null) {\n const attrs = match[1]\n const action = attrs.match(/\\baction=\"([^\"]*)\"/)?.[1] ?? \"?\"\n const target = attrs.match(/\\btarget=\"([^\"]*)\"/)?.[1]\n streams.push(target ? `${action} → #${target}` : action)\n }\n return streams\n }\n\n // console.group ONE dispatch (issue #108). Carries NAMES + outcomes ONLY — the\n // signed token VALUE and every field/param VALUE are deliberately absent (they\n // may be sensitive; the whole point is observability without leaking data). The\n // caller passes the info it already holds so nothing is recomputed or re-fetched:\n // { action, paramNames, fieldNames, encoding, status, streams, tokenRefreshed, ms }\n // `console.groupCollapsed` keeps the console tidy (one collapsed line per action).\n #logDispatch(info) {\n const { action, status, ms } = info\n // The client can't name the component CLASS (it's inside the signed, opaque\n // token — never decoded here), but the root's id is the stable client-side\n // handle (e.g. #todo_42), so the header reads `reactive #todo_42 rename → …`.\n const who = this.element?.id ? `#${this.element.id} ` : \"\"\n const header = `reactive ${who}${action} → ${status ?? \"—\"} (${Math.round(ms)}ms)`\n /* eslint-disable no-console */\n console.groupCollapsed(header)\n console.log(`params: [${info.paramNames.join(\", \")}] + collected: [${info.fieldNames.join(\", \")}]`)\n console.log(`encoding: ${info.encoding}`)\n if (info.streams.length) console.log(`streams: ${info.streams.join(\" \")}`)\n console.log(`token: ${info.tokenRefreshed ? \"refreshed ✓\" : \"unchanged\"}`)\n console.groupEnd()\n /* eslint-enable no-console */\n }\n\n async #perform(action, params, inverse, settle) {\n // Auto-collect named field values inside this component so a button-\n // triggered action still receives sibling inputs (Livewire-style), plus any\n // chosen file inputs in the SAME walk. Explicit params\n // (data-reactive-params-param) win over collected fields.\n const { fields, files } = this.#collectFields()\n // Parse the explicit trigger params ONCE — reused below for allParams and, on\n // the debug path, for the params-only name list (so the trace can show\n // `params: [...]` distinct from the collected `+ collected: [...]`). #parseParams\n // is pure (a fresh object from a JSON string, or the same object by reference\n // when already parsed); allParams spreads a COPY and nothing mutates parsedParams\n // downstream (JSON.stringify / #buildFormData read allParams, a new object).\n const parsedParams = this.#parseParams(params)\n const allParams = { ...fields, ...parsedParams }\n const token = this.#currentToken\n\n // File/multipart path (issue #34): if THIS root has a populated\n // <input type=\"file\">, the action can't be JSON (JSON.stringify drops the\n // File). Send FormData instead — token + act + scalar params as fields, each\n // chosen file appended. The morph/token machinery downstream is identical;\n // only the request ENCODING differs when files are present.\n const multipart = files.length > 0\n const body = multipart\n ? this.#buildFormData(token, action, allParams, files)\n : JSON.stringify({ token, act: action, params: allParams })\n\n // Client debug mode (issue #108): build the trace object ONLY when debug is on\n // (one nil-check otherwise — zero cost off). It carries NAMES only: the\n // explicit trigger param names and the collected sibling field names, split so\n // the group shows `params: [...] + collected: [...]`. status/streams/\n // tokenRefreshed are filled in at the branch that knows them; the group is\n // emitted once in the finally so EVERY exit (success or any failure) logs.\n const debug = this.#debugEnabled()\n ? {\n action,\n paramNames: Object.keys(parsedParams),\n fieldNames: Object.keys(fields),\n encoding: multipart ? \"multipart\" : \"json\",\n status: null,\n streams: [],\n tokenRefreshed: false,\n started: this.#debugNow(),\n }\n : null\n\n // aria-busy on the root is now driven by the loading pending counter\n // (#applyLoading, applied at ENQUEUE so it covers the queue wait too), not\n // set here. #settleLoading in the finally clears it — see #enqueue.\n\n // Latency simulator (issue #102): the busy window (aria-busy + loading state)\n // is already applied at enqueue, so awaiting the configured delay HERE — after\n // that window opened and before the fetch — is what makes it visible on\n // localhost. A null/absent/non-positive sessionStorage key is a no-op (zero\n // production surface), so this line vanishes for any app that never opts in.\n await this.#maybeSimulateLatency()\n\n try {\n // Offline gate (issue #101), authoritative at the NETWORK BOUNDARY (send\n // time), not at enqueue. A click can enqueue while online and reach here\n // after going offline (a debounced/queued request, a rapid transition) —\n // gating in #perform makes the kind consistently \"offline\" for that whole\n // condition instead of leaking through as a \"network\" fetch throw. The\n // fetch never fires, so the edit is not half-sent; the finally still runs\n // (settle clears loading), the optimistic hint reverts, and retry() (which\n // re-enters #perform) re-checks and sends once back online.\n if (navigator.onLine === false) {\n this.#revertOptimistic(inverse)\n this.#markError(\"offline\")\n this.#emitError(action, params, allParams, { kind: \"offline\" })\n return\n }\n\n let response\n try {\n const headers = {\n Accept: \"text/vnd.turbo-stream.html\",\n \"X-CSRF-Token\": this.#csrfToken(),\n }\n // For JSON we declare the content type; for multipart we must NOT — the\n // browser sets `multipart/form-data; boundary=…` itself, and overriding it\n // would strip the boundary and corrupt the body server-side.\n if (!multipart) headers[\"Content-Type\"] = \"application/json\"\n // Send the pgbus SSE connection id (if subscribed) so the server can\n // exclude this connection from its own broadcast echo — the actor\n // already gets the action's HTTP response. Harmless without pgbus.\n const connectionId = this.#connectionId()\n if (connectionId) headers[\"X-Pgbus-Connection\"] = connectionId\n\n // ONLY `fetch` itself is the network boundary — offline, DNS, a reset\n // connection. Everything below this inner try (reading the body,\n // extracting the token, handing streams to Turbo) runs AFTER the\n // server already processed the mutation, so none of it belongs in the\n // `kind: \"network\"` / retriable bucket (CodeRabbit review on #89): if\n // renderStreamMessage throws, retry()ing would re-POST an action the\n // server already completed — see the outer catch below. (NOT a\n // reactive:applied LISTENER throwing — per the DOM spec, dispatchEvent\n // never propagates a listener's exception back to its caller, so that\n // case can't reach this catch at all; verified in the JS test suite.)\n response = await fetch(this.#actionPath(), {\n method: \"POST\",\n headers,\n body,\n credentials: \"same-origin\",\n // Bound the request (issue #101): a server that never answers used to\n // wedge this.queue forever (the finally that clears aria-busy/loading\n // never ran). AbortSignal.timeout(ms) aborts the fetch after the\n // configured window; the abort surfaces in this catch as a\n // DOMException named \"TimeoutError\" (see the branch below).\n signal: AbortSignal.timeout(this.#timeoutMs()),\n })\n } catch (error) {\n console.error(\"[phlex-reactive] action error\", error)\n this.#revertOptimistic(inverse)\n // AbortSignal.timeout() rejects with a DOMException named \"TimeoutError\"\n // (a manual AbortController.abort() would be \"AbortError\" — we don't use\n // one, but accept it too for robustness). A timeout is NOT \"offline\":\n // the request left and the server didn't answer in time; connectivity is\n // unknown. So do NOT clone the offline-fallback template or mark network\n // — just fire kind:\"timeout\" (retriable). The queue still advances\n // (#perform returns, never rejects), so the hung request un-wedges it.\n if (error?.name === \"TimeoutError\" || error?.name === \"AbortError\") {\n this.#markError(\"timeout\")\n this.#emitError(action, params, allParams, { kind: \"timeout\" })\n return\n }\n // No server reached — nothing to render (issue #100). Clone a\n // server-rendered <template data-reactive-error-flash> into the flash\n // region as an offline fallback. The template is rendered by the app\n // (trusted), so this is a pure clone — no client templating of data.\n this.#renderNetworkFallback()\n this.#markError(\"network\")\n this.#emitError(action, params, allParams, { kind: \"network\" })\n return\n }\n\n // Debug (issue #108): the server answered — record the status for the trace\n // now, so every response branch below (redirected/http/content-type/ok) logs\n // it. The transport-failure branches above return before here (they have no\n // status); their group still fires from the finally with status null → \"—\".\n if (debug) debug.status = response.status\n\n if (response.redirected) {\n console.error(\"[phlex-reactive] action was redirected (auth/CSRF?) — no update applied\")\n this.#revertOptimistic(inverse)\n this.#markError(\"redirected\")\n this.#emitError(action, params, allParams, { kind: \"redirected\", status: response.status })\n return\n }\n if (!response.ok) {\n const errorBody = await response.text()\n console.error(`[phlex-reactive] action failed: HTTP ${response.status}`, errorBody)\n this.#revertOptimistic(inverse)\n // Render a non-OK turbo-stream body so a server-rendered error flash\n // (an error_flash rescue, or a status: :unprocessable_entity validation\n // reply from a plain controller) is actually SHOWN — instead of being\n // read only for the console (issue #100). #extractToken is run as usual;\n // it NO-OPS when no stream re-renders our id, so a 400 InvalidToken body\n // never refreshes the held identity token (which is not a nonce and stays\n // retry-valid — do not \"fix\" that). A non-turbo-stream body is left to the\n // console.error above (an HTML error page must not be handed to Turbo).\n if ((response.headers.get(\"Content-Type\") || \"\").includes(\"turbo-stream\")) {\n const fresh = this.#extractToken(errorBody)\n this.#currentToken = fresh ?? this.#currentToken\n if (debug) this.#debugRecordBody(debug, errorBody, fresh)\n window.Turbo.renderStreamMessage(errorBody)\n }\n this.#markError(\"http\")\n this.#emitError(action, params, allParams, { kind: \"http\", status: response.status, body: errorBody })\n return\n }\n\n const contentType = response.headers.get(\"Content-Type\") || \"\"\n if (!contentType.includes(\"turbo-stream\")) {\n console.error(`[phlex-reactive] expected a turbo-stream, got \"${contentType}\" — no update applied`)\n this.#revertOptimistic(inverse)\n this.#markError(\"content-type\")\n this.#emitError(action, params, allParams, { kind: \"content-type\", status: response.status })\n return\n }\n\n const html = await response.text()\n // Capture the new token from the response synchronously, so the next\n // queued request uses it without waiting for the async DOM morph.\n const fresh = this.#extractToken(html)\n this.#currentToken = fresh ?? this.#currentToken\n // Debug (issue #108): record the stream actions/targets + whether a refresh\n // arrived, from the body we JUST read (reuse — no second text() read). Never\n // the token or template contents.\n if (debug) this.#debugRecordBody(debug, html, fresh)\n // Turbo applies the <turbo-stream> ops by id. A plain replace is an\n // outerHTML swap (focus on the replaced subtree is lost); a method=\"morph\"\n // replace (Response.morph) or an update morphs in place, preserving the\n // focused input + caret on unchanged nodes — see issue #28.\n window.Turbo.renderStreamMessage(html)\n // A successful apply CLEARS any prior failure marker (issue #100), so\n // error-driven CSS on the root (a red border, a shake) resets on recovery.\n this.#clearError()\n // Lifecycle hook (issue #79): the streams were HANDED TO Turbo — a\n // renderStreamMessage applies asynchronously, so the DOM mutation may\n // complete a tick later. Apps needing post-morph timing listen to Turbo's\n // own events; this one is for \"the action round trip succeeded\".\n this.#emit(\"reactive:applied\", { action, params: allParams, html })\n } catch (error) {\n // The server already processed this action successfully (we're past the\n // fetch) — a throw here is a CLIENT-side apply failure (a malformed\n // response, a broken Turbo render — NOT a reactive:applied listener\n // throw, which dispatchEvent never propagates here), not a transport\n // failure. kind: \"apply\" carries NO retry() — retrying would re-POST an\n // action the server already completed.\n console.error(\"[phlex-reactive] action error\", error)\n this.#revertOptimistic(inverse)\n this.#emit(\"reactive:error\", { action, params: allParams, kind: \"apply\" })\n } finally {\n // Settle the loading state (issue #99): decrement the pending counter,\n // drop the trigger's/root's busy tokens, restore disabled/text/class —\n // guarded so a morph-replaced trigger is never clobbered. Runs on EVERY\n // exit (success, every failure branch, or an apply throw).\n settle?.()\n // Debug (issue #108): emit the group here so EVERY exit path logs exactly\n // once — success, any transport/response failure, or an apply throw. Null\n // when debug is off (zero cost). The round-trip ms is measured now, at the\n // finally, so it spans the whole #perform (fetch + apply) regardless of exit.\n if (debug) this.#logDispatch({ ...debug, ms: this.#debugNow() - debug.started })\n }\n }\n\n // Debug (issue #108): fold the response body #perform already read into the\n // trace — the stream action/target pairs and whether a token refresh arrived\n // (a boolean; the token VALUE is intentionally not stored). Shared by the\n // success and the non-OK-turbo-stream branches so both log the same shape.\n #debugRecordBody(debug, body, freshToken) {\n debug.streams = this.#debugStreams(body)\n debug.tokenRefreshed = freshToken != null\n }\n\n get #currentToken() {\n return this.#tokenCache ?? this.tokenValue\n }\n\n set #currentToken(value) {\n this.#tokenCache = value\n }\n\n // Read the next token for THIS controller — the one that re-renders THIS\n // element's id, never just the first token in the body (issue #46). On a\n // collection of REACTIVE rows the prepended/appended ROW carries its OWN\n // data-reactive-token-value and it sorts FIRST in the response; the list's own\n // fresh token rides a trailing `reactive:token` stream targeting the container.\n // Grabbing the first match stored the ROW's token, so the list's SECOND\n // dispatch sent a row token → failed verification → add-once-only. This mirrors\n // the server's carries_token_for? (#44): a stream carries OUR token only when it\n // RE-RENDERS our id (reactive:token / replace / update of `this.element.id`) —\n // append/prepend insert children and never count. Returns undefined when no\n // stream re-renders our id, so #currentToken keeps its existing value.\n #extractToken(html) {\n const id = this.element.id\n if (!id) {\n // No id to self-match (shouldn't happen for a reactive root). Fall back to\n // the legacy first-token behavior so a single-component response still works.\n return html.match(/data-reactive-token-value=\"([^\"]+)\"/)?.[1]\n }\n\n const { token, self } = this.#tokenRegexes(id)\n\n // The dedicated token-only refresh for THIS element (partial updates / the\n // collection container) — an attribute on the <turbo-stream> itself.\n const tokenStream = html.match(token)\n if (tokenStream) return tokenStream[1]\n\n // A full self re-render: a replace/update of THIS element whose template root\n // carries the fresh token. Scope the token search to that one stream so a\n // sibling/child token elsewhere in the body can't leak in.\n const selfStream = html.match(self)\n if (selfStream) return selfStream[1].match(/data-reactive-token-value=\"([^\"]+)\"/)?.[1]\n\n // Nothing re-rendered our id — keep the current token.\n return undefined\n }\n\n // The two per-id RegExps #extractToken uses to self-match this element's next\n // token (issue #118). `this.element.id` is page-stable, so these are compiled\n // ONCE and reused across every response — instead of allocating two fresh\n // RegExps per round trip. The memo is KEYED ON THE ID and rebuilt when it\n // changes: a re-render that re-identifies the root must scan for the NEW target,\n // never the stale one (or the token would freeze — see the id-change bun test).\n // The PATTERNS are byte-identical to the pre-memo inline literals; only their\n // allocation moved.\n #tokenRegexes(id) {\n const cache = this.#tokenRegexCache\n if (cache && cache.id === id) return cache\n const escaped = escapeRegExp(id)\n return (this.#tokenRegexCache = {\n id,\n token: new RegExp(\n `<turbo-stream\\\\b[^>]*\\\\baction=\"reactive:token\"[^>]*\\\\btarget=\"${escaped}\"[^>]*\\\\bdata-reactive-token-value=\"([^\"]+)\"`,\n ),\n self: new RegExp(\n `<turbo-stream\\\\b[^>]*\\\\baction=\"(?:replace|update)\"[^>]*\\\\btarget=\"${escaped}\"[^>]*>([\\\\s\\\\S]*?)</turbo-stream>`,\n ),\n })\n }\n\n // True when `el` is collected by THIS reactive root and not by a nested one.\n // A reactive component can be rendered inside another (both are\n // data-controller=\"reactive\" roots). querySelectorAll() descends into nested\n // roots, so without this guard an outer action would sweep the inner roots'\n // inputs into its own params (issue #15). An element belongs to this root iff\n // its nearest [data-controller~=\"reactive\"] ancestor is this.element.\n #ownsField(el) {\n return el.closest('[data-controller~=\"reactive\"]') === this.element\n }\n\n // A per-op ownership PREDICATE — the issue #117 fast path over issue #15\n // scoping. #ownsField answers \"is this element mine, not a nested reactive\n // root's\" with a per-element closest() walk; on a wide form or every keystroke\n // that walk runs per matched field. This hoists the DECISION to once per op.\n //\n // HYBRID GATE — closest() stays the source of truth; the nested-root query only\n // decides whether the fast path is SAFE:\n // * Fast path (overwhelmingly common): this root contains NO nested reactive\n // roots, so every element the caller's querySelectorAll returned is already\n // a direct descendant of this.element with no intervening reactive root —\n // it is ours. Return a constant-true predicate and skip the per-field\n // closest() walk entirely. That is the whole win.\n // * Nested case: fall back to the UNCHANGED #ownsField closest() check, so\n // scoping is byte-identical to before. We deliberately do NOT use\n // contains() here — the closest() form needs no node to implement\n // contains(), and on a real DOM the two agree for a descendant of\n // this.element (el.closest('[data-controller~=\"reactive\"]') === this.element\n // iff no nested reactive-root descendant contains el).\n //\n // Computed ONCE per dispatch-scoped op (per #collectFields call, per recompute,\n // per #listnavOptions) and NEVER stored on the instance — a morph replaces\n // nodes, so a cached predicate would close over stale roots.\n #ownershipFilter() {\n const nested = this.element.querySelectorAll('[data-controller~=\"reactive\"]')\n if (nested.length === 0) return () => true\n return (el) => this.#ownsField(el)\n }\n\n // One walk over THIS root's named controls (not a nested reactive root's),\n // returning both the scalar `fields` and any chosen `files`. The ownership\n // predicate is hoisted ONCE (issue #117) via #ownershipFilter — in the common\n // no-nested-root case it is a constant true, so we skip a closest() walk per\n // field. A file input's `.value` is the useless \"C:\\fakepath\\…\" string, never a\n // scalar — so its chosen files are collected separately (honoring `multiple`)\n // and it adds no phantom blank value (issue #34). An empty `files` keeps the\n // JSON path.\n #collectFields() {\n const fields = {}\n const files = []\n const owns = this.#ownershipFilter() // compute ONCE per dispatch (issue #117)\n this.element.querySelectorAll(\"input[name], select[name], textarea[name]\").forEach((field) => {\n if (!owns(field)) return\n if (field.type === \"file\") {\n // Carry the input's `multiple` flag so #buildFormData keeps the array\n // shape (params[name][]) even when the user picked exactly one file —\n // otherwise a [:file] schema would see a lone scalar upload and drop it.\n for (const file of field.files ?? []) files.push({ name: field.name, file, multiple: field.multiple })\n } else if (field.type === \"checkbox\") {\n fields[field.name] = field.checked\n } else if (field.type === \"radio\") {\n if (field.checked) fields[field.name] = field.value\n } else {\n fields[field.name] = field.value\n }\n })\n // Named rich-text / custom editors (lexxy-editor, trix-editor) and bare\n // [contenteditable]. These aren't input/select/textarea, so the query above\n // skips them — without this, a reactive save posts an empty value and\n // silently wipes the field (issue #8). Read whatever the element exposes:\n // a custom editor's serialized `.value`, else its contenteditable text.\n // Only fill a name the standard controls left absent or empty, so a synced\n // hidden input (e.g. Trix mirrors into one) still wins when populated.\n this.element\n .querySelectorAll(\"[name]:is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])\")\n .forEach((el) => {\n if (!owns(el)) return // reuse the SAME hoisted predicate (nested reactive root — issue #15)\n // A plain element (e.g. a <div contenteditable>) has no `name` IDL\n // property — only the attribute — so read getAttribute, not el.name.\n const name = el.getAttribute(\"name\")\n if (!name) return\n const existing = fields[name]\n if (existing == null || existing === \"\") {\n fields[name] = el.value ?? el.textContent ?? el.innerHTML ?? \"\"\n }\n })\n return { fields, files }\n }\n\n // Re-compute the dirty flag for EVERY field this root owns in one pass (issue\n // #103), then reflect the total onto the root. Called on an owned field's input\n // (trackDirty), on connect (baseline seed), and after a turbo:morph-element\n // re-render (fresh default* attrs). dirty = current ≠ the DOM's own default:\n // checkbox/radio → checked !== defaultChecked\n // select → some option.selected !== option.defaultSelected\n // else → value !== defaultValue\n // A full pass (not per-target) is REQUIRED: a radio group's previously-checked\n // radio flips to checked=false with NO input event, so per-target toggling\n // would leave its flag stale. File inputs are skipped — a file has no server\n // default baseline. Per-dirty-field data-reactive-dirty=\"true\" (\"true\" STRING,\n // not a valueless boolean attr — mirrors the on() flag convention); the root\n // carries data-reactive-dirty=\"<count>\" and DROPS the attr at zero, so\n // `[data-reactive-dirty]` styles the whole form and `[data-reactive-dirty]`\n // on a field styles just the changed control — both pure CSS, zero JS.\n #scanDirty() {\n // Runs at bootstrap (the connect baseline seed) as well as on input/morph, so\n // it must never throw — degrade to a no-op if the root can't be queried (a real\n // reactive root always can; this guards minimal/test element stubs).\n if (typeof this.element?.querySelectorAll !== \"function\") return\n\n let count = 0\n this.element.querySelectorAll(\"input[name], select[name], textarea[name]\").forEach((field) => {\n if (!this.#ownsField(field)) return // skip a nested reactive root's fields (issue #15)\n if (field.type === \"file\") return // no server default baseline to diff against\n\n if (this.#fieldDirty(field)) {\n field.setAttribute(\"data-reactive-dirty\", \"true\")\n count++\n } else {\n field.removeAttribute(\"data-reactive-dirty\")\n }\n })\n\n if (count > 0) this.element.setAttribute(\"data-reactive-dirty\", String(count))\n else this.element.removeAttribute(\"data-reactive-dirty\")\n }\n\n // Whether a single owned control differs from its server-rendered default.\n #fieldDirty(field) {\n if (field.type === \"checkbox\" || field.type === \"radio\") {\n return field.checked !== field.defaultChecked\n }\n if (field.tag === \"select\" || field.options) {\n // Any option whose selected state diverges from its defaultSelected. Guard\n // for a stub/absent options list (degrade to clean).\n return Array.from(field.options ?? []).some((o) => o.selected !== o.defaultSelected)\n }\n return field.value !== field.defaultValue\n }\n\n // The live dirty-field count, re-derived from the DOM (never a cached snapshot)\n // — the source of truth for the warn_unsaved guard's gate.\n #dirtyCount() {\n const raw = this.element.getAttribute?.(\"data-reactive-dirty\")\n const n = Number(raw)\n return Number.isFinite(n) && n > 0 ? n : 0\n }\n\n // Arm the navigate-away guard (warn_unsaved: true, issue #103). beforeunload\n // blocks a real browser unload; turbo:before-visit blocks a Turbo in-app\n // navigation (it does NOT fire on restoration visits — the documented gap).\n // Both read the LIVE dirty count, so a clean form never blocks. Handlers are\n // stored so disconnect() removes exactly them.\n #armUnsavedGuard() {\n if (typeof window === \"undefined\" || typeof window.addEventListener !== \"function\") return\n\n this.#boundBeforeUnload = (event) => {\n if (this.#dirtyCount() === 0) return undefined\n // The spec dance: preventDefault + a truthy returnValue triggers the native\n // \"leave site?\" prompt. The string is legacy (modern browsers show their own\n // copy) but must be non-empty/truthy to arm the dialog.\n event.preventDefault()\n event.returnValue = \"You have unsaved changes.\"\n return event.returnValue\n }\n this.#boundBeforeVisit = (event) => {\n if (this.#dirtyCount() === 0) return\n const ok = typeof window.confirm === \"function\" ? window.confirm(\"You have unsaved changes. Leave anyway?\") : true\n if (!ok) event.preventDefault?.()\n }\n\n window.addEventListener(\"beforeunload\", this.#boundBeforeUnload)\n window.addEventListener(\"turbo:before-visit\", this.#boundBeforeVisit)\n }\n\n // Remove the dirty-tracking listeners on disconnect (Turbo morph/navigation) so\n // a morph re-scan or a navigate-away guard never runs against a detached root.\n #teardownDirtyTracking() {\n if (this.#boundScanDirty) {\n this.element.removeEventListener?.(\"turbo:morph-element\", this.#boundScanDirty)\n this.#boundScanDirty = undefined\n }\n if (typeof window !== \"undefined\" && typeof window.removeEventListener === \"function\") {\n if (this.#boundBeforeUnload) window.removeEventListener(\"beforeunload\", this.#boundBeforeUnload)\n if (this.#boundBeforeVisit) window.removeEventListener(\"turbo:before-visit\", this.#boundBeforeVisit)\n }\n this.#boundBeforeUnload = undefined\n this.#boundBeforeVisit = undefined\n }\n\n // Whether this root owns a show binding (issue #161) or declares cross-root\n // show targets (issue #164) — the connect() gate, so a component with\n // neither pays only this probe (the #dirtyTrackingEnabled precedent). A\n // NESTED root's bindings don't count: its own controller instance syncs them\n // (issue #15 ownership). The targets attr is checked FIRST — one\n // getAttribute, cheaper than the binding walk.\n #showSyncEnabled() {\n if (this.element.getAttribute?.(\"data-reactive-show-targets\")) return true\n // Single-field bindings carry -field; compound all:/any: bindings (issue\n // #176) carry data-reactive-show and have NO single controlling field, so\n // both selectors gate the sync.\n const nodes = this.element.querySelectorAll?.(SHOW_BINDING_SELECTOR) ?? []\n for (const el of nodes) if (this.#ownsField(el)) return true\n return false\n }\n\n // Re-evaluate every OWNED show binding in one pass (issue #161): read the\n // controlling field's current value, evaluate the declared literal predicate,\n // toggle `hidden`. A full pass (not per-target) for the same reason as\n // #scanDirty — a radio group's deselected radio fires no event — and because\n // several bindings can hang off one field (the value read is memoized per\n // pass). A binding whose field can't be resolved, or whose predicate is\n // malformed, leaves visibility ALONE — a bad binding must never break or\n // blank the page (client-side default-deny).\n #syncShow() {\n if (typeof this.element?.querySelectorAll !== \"function\") return\n\n const owns = this.#ownershipFilter()\n const values = new Map()\n // A memoized resolver shared by every binding in this pass — a field driving\n // several bindings (and several terms of a compound) reads exactly once.\n const fieldValue = (name) => {\n if (!values.has(name)) values.set(name, this.#showFieldValue(name, owns))\n return values.get(name)\n }\n for (const el of this.element.querySelectorAll(SHOW_BINDING_SELECTOR)) {\n if (!owns(el)) continue // a nested root's binding is its own controller's job\n\n // Compound all:/any: (issue #176 part A): no single -field attr; parse the\n // JSON payload and fold its per-field terms.\n const compoundRaw = el.getAttribute(\"data-reactive-show\")\n if (compoundRaw !== null) {\n const match = compoundShowMatches(parseShowCompound(compoundRaw), fieldValue)\n if (match !== null) el.hidden = !match\n continue\n }\n\n const name = el.getAttribute(\"data-reactive-show-field\")\n if (!name) continue\n const value = fieldValue(name)\n if (value === null) continue // no owned field with that name — leave it be\n const match = showBindingMatches(el, value)\n if (match === null) continue // malformed predicate — warned + skipped\n el.hidden = !match\n }\n\n // The cross-root pass (issue #164) shares the same owned-field memo, so a\n // field driving both an owned binding and an outside target reads once.\n this.#syncShowTargets(owns, values)\n }\n\n // Apply the declared cross-root show targets (issue #164) — the visibility\n // parallel of #applyComputeMirrors. For each declared field: read the OWNED\n // field's current value (never a nested root's — you can only drive outside\n // visibility from a field this root owns), then for each \"#id\" → predicate\n // entry: guard the selector id-only (warn-and-skip; the Ruby helper raised\n // at declare time — two-sided default-deny), resolve it DOCUMENT-WIDE, and\n // toggle `hidden`. A target id not on the page is silently skipped (an\n // unrendered tab pane is normal); a malformed predicate warn-skips its one\n // target while siblings still apply. With no map declared this is one\n // getAttribute and out.\n #syncShowTargets(owns, values) {\n const map = this.#parseShowTargets()\n for (const [name, targets] of Object.entries(map)) {\n if (!targets || typeof targets !== \"object\" || Array.isArray(targets)) continue\n if (!values.has(name)) values.set(name, this.#showFieldValue(name, owns))\n const value = values.get(name)\n if (value === null) continue // no owned field with that name — leave them be\n for (const [selector, pred] of Object.entries(targets)) {\n if (!guardShowTargetSelector(selector)) continue\n const match = showPredicateMatches(pred, value)\n if (match === null) {\n console.warn(`[phlex-reactive] malformed reactive_show_targets predicate for ${selector} — skipped`)\n continue\n }\n for (const node of document.querySelectorAll(selector)) node.hidden = !match\n }\n }\n }\n\n // The declared cross-root show-target map (issue #164): a JSON object of\n // { field: { \"#id\": predicate } } from data-reactive-show-targets (emitted\n // by reactive_show_targets on the root). Absent degrades to {}; malformed\n // degrades to {} WITH a warn — never a throw (the #parseComputeMirror\n // contract), but never silent either: the likeliest cause is TWO\n // reactive_show_targets calls on one root, whose JSON strings Phlex `mix`\n // space-joined into an unparseable attr. The warn names the fix.\n #parseShowTargets() {\n const raw = this.element.getAttribute?.(\"data-reactive-show-targets\")\n if (!raw) return {}\n try {\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed\n } catch {\n // fall through to the shared warn below\n }\n console.warn(\n \"[phlex-reactive] malformed data-reactive-show-targets — ignored. \" +\n \"Did two reactive_show_targets calls collide on one root? Declare every field in ONE call: \" +\n \"reactive_show_targets(mode: { ... }, kind: { ... })\"\n )\n return {}\n }\n\n // The current value of the OWNED field controlling a show binding, as the\n // string the literal predicate compares against. Mirrors #collectFields'\n // per-kind reads: a checkbox reports its checked state (\"true\"/\"false\" — its\n // .value is the constant \"on\", and the checkbox wins over the hidden input\n // Rails pairs with it); a radio group reports the CHECKED radio's value (\"\"\n // when none is); anything else reports .value first-wins. Returns null when\n // no owned field carries the name — the caller then leaves visibility alone.\n #showFieldValue(name, owns) {\n let sawRadio = false\n let first = null\n for (const el of this.element.querySelectorAll(`[name=\"${name}\"]`)) {\n if (!owns(el)) continue\n if (el.type === \"checkbox\") return el.checked ? \"true\" : \"false\"\n if (el.type === \"radio\") {\n if (el.checked) return el.value ?? \"\"\n sawRadio = true\n continue\n }\n first ??= el\n }\n if (first) return first.value ?? \"\"\n return sawRadio ? \"\" : null\n }\n\n // Remove the show-sync listeners on disconnect, so a stray event after a\n // Turbo morph/navigation never re-evaluates against a detached root.\n #teardownShowSync() {\n if (!this.#boundSyncShow) return\n this.element.removeEventListener?.(\"input\", this.#boundSyncShow)\n this.element.removeEventListener?.(\"change\", this.#boundSyncShow)\n this.element.removeEventListener?.(\"turbo:morph-element\", this.#boundSyncShow)\n this.#boundSyncShow = undefined\n }\n\n // Whether this root declares an option filter (issue #163) — the connect()\n // gate. reactive_filter always emits input + option together, so requiring\n // BOTH also default-denies a half-built hand-authored binding.\n #filterEnabled() {\n return !!(\n this.element.getAttribute?.(\"data-reactive-filter-input\") &&\n this.element.getAttribute?.(\"data-reactive-filter-option\")\n )\n }\n\n // Whether a delegated input event came from the NAMED filter input (issue\n // #163). Anything else — another field's keystroke, a target without\n // matches() — skips the filter pass (the morph re-sync path bypasses this).\n #filterInputEvent(event) {\n const selector = this.element.getAttribute(\"data-reactive-filter-input\")\n return !!selector && typeof event.target?.matches === \"function\" && event.target.matches(selector)\n }\n\n // Re-apply the filter in one pass (issue #163): lowercase the named input's\n // current value, toggle `hidden` on every OWNED option by a substring match\n // against its haystack (data-reactive-filter-text, falling back to the\n // option's own text), collapse any group whose every contained option is\n // hidden, and reveal the empty target at 0 visible. A filtered-out option\n // also loses its listnav highlight so Enter can never pick an invisible row.\n // No owned input → leave visibility ALONE — a binding that can't resolve\n // must never break or blank the page (client-side default-deny). All\n // selectors resolve within this root, skipping nested reactive roots'\n // elements (issue #15 ownership; the predicate is hoisted once per pass).\n #syncFilter() {\n if (typeof this.element?.querySelectorAll !== \"function\") return\n const inputSelector = this.element.getAttribute(\"data-reactive-filter-input\")\n const optionSelector = this.element.getAttribute(\"data-reactive-filter-option\")\n if (!inputSelector || !optionSelector) return\n\n const owns = this.#ownershipFilter()\n const input = [...this.element.querySelectorAll(inputSelector)].find(owns)\n if (!input) return\n\n const query = (input.value ?? \"\").trim().toLowerCase()\n let visible = 0\n for (const el of this.element.querySelectorAll(optionSelector)) {\n if (!owns(el)) continue // a nested root's option is its own controller's job\n const haystack = (el.getAttribute(\"data-reactive-filter-text\") ?? el.textContent ?? \"\").toLowerCase()\n const hidden = query !== \"\" && !haystack.includes(query)\n el.hidden = hidden\n if (hidden) el.removeAttribute(\"data-reactive-highlighted\")\n else visible++\n }\n\n const groupSelector = this.element.getAttribute(\"data-reactive-filter-group\")\n if (groupSelector) {\n for (const group of this.element.querySelectorAll(groupSelector)) {\n if (!owns(group)) continue\n const contained = [...group.querySelectorAll(optionSelector)].filter(owns)\n // A group with no options isn't this filter's to decide — server state\n // stands (it may be a header the app toggles by other means).\n if (contained.length === 0) continue\n group.hidden = contained.every((el) => el.hidden)\n }\n }\n\n const emptySelector = this.element.getAttribute(\"data-reactive-filter-empty\")\n if (emptySelector) {\n for (const el of this.element.querySelectorAll(emptySelector)) {\n if (owns(el)) el.hidden = visible > 0\n }\n }\n }\n\n // Remove the filter-sync listeners on disconnect, so a stray event after a\n // Turbo morph/navigation never re-filters against a detached root.\n #teardownFilterSync() {\n if (!this.#boundSyncFilter) return\n this.element.removeEventListener?.(\"input\", this.#boundSyncFilter)\n this.element.removeEventListener?.(\"turbo:morph-element\", this.#boundSyncFilter)\n this.#boundSyncFilter = undefined\n }\n\n // Build the multipart body (issue #34). `token`/`act` are flat fields the\n // endpoint reads from params[:token]/params[:act]; scalar params nest under\n // params[<key>] (Rails parses the bracket into params[:params]); each file is\n // appended under params[<name>] (single) — a second file with the same name\n // (a `multiple` picker, several inputs sharing a name) is sent as\n // params[<name>][] so Rails coerces it to an array for a [:file] schema.\n #buildFormData(token, action, params, files) {\n const fd = new FormData()\n fd.append(\"token\", token)\n fd.append(\"act\", action)\n for (const [key, value] of Object.entries(params)) {\n this.#appendField(fd, `params[${key}]`, value)\n }\n const multiNames = this.#multiFileNames(files)\n for (const { name, file, multiple } of files) {\n // params[name][] when the input is `multiple` (array shape even for one\n // file) OR the name repeats across inputs; otherwise a lone scalar file.\n const asArray = multiple || multiNames.has(name)\n const key = asArray ? `params[${name}][]` : `params[${name}]`\n fd.append(key, file, file.name)\n }\n return fd\n }\n\n // Append a param leaf to FormData under its bracketed key. FormData carries\n // only strings, so a NON-scalar param (a nested object or an array) is\n // bracket-EXPANDED into params[key][sub] / params[key][index][...] fields —\n // the SAME Rails-form shape the server's expand_bracket_keys / array_values\n // already parse, so a JSON body and a multipart body coerce identically\n // (issue #39). Previously a non-scalar was JSON.stringify'd into one\n // params[key]='<json>' field, which the server received as an un-decodable\n // String leaf and DROPPED (nested hash -> {}, array -> key removed).\n //\n // Arrays use NUMERIC indices (params[key][0], params[key][1]) — required for\n // an array-of-hash so each element's sub-keys stay grouped and the server's\n // index-hash sort rebuilds order; params[key][] would collapse them. A scalar\n // (string/number/boolean) is one string field, mirroring the JSON wire shape\n // (the server's :boolean/:integer casts read \"true\"/\"42\"). null/undefined is\n // an empty field. An EMPTY array/object emits NOTHING — FormData can't carry\n // []/{}, so the key is omitted and the action's keyword default applies (this\n // differs from the JSON path, where an explicit [] coerces to an empty array).\n #appendField(fd, key, value) {\n if (value == null) {\n fd.append(key, \"\")\n } else if (Array.isArray(value)) {\n value.forEach((element, index) => this.#appendField(fd, `${key}[${index}]`, element))\n } else if (typeof value === \"object\") {\n for (const [subKey, subValue] of Object.entries(value)) {\n this.#appendField(fd, `${key}[${subKey}]`, subValue)\n }\n } else {\n fd.append(key, String(value))\n }\n }\n\n // Names that appear more than once across the chosen files (a `multiple`\n // picker, or several file inputs sharing a name) — those go to params[name][]\n // so the server sees an array; a lone file stays params[name].\n #multiFileNames(files) {\n const counts = new Map()\n for (const { name } of files) counts.set(name, (counts.get(name) ?? 0) + 1)\n return new Set([...counts].filter(([, n]) => n > 1).map(([name]) => name))\n }\n\n #parseParams(raw) {\n if (!raw) return {}\n try {\n return typeof raw === \"string\" ? JSON.parse(raw) : raw\n } catch {\n return {}\n }\n }\n\n // Stimulus typecasts a JSON param to the parsed array already; a raw string\n // (hand-built attr, non-typecasting harness) is parsed here. Delegates to the\n // shared parseOps so the controller and the reactive:js stream action (issue\n // #97) treat a malformed ops attr identically (→ [], never a throw).\n #parseOps(raw) {\n return parseOps(raw)\n }\n\n // Interpret a [[name, args], ...] op list against this root (issue #95),\n // scoping each op's targets to this controller's own root via #opTargets (the\n // nested-reactive-root ownership filter, issue #15). The whitelist + skip\n // logic lives in the shared applyOps so runOps and the reactive:js stream\n // action interpret the SAME vocabulary the SAME way (client-side default-deny).\n #applyOps(list) {\n applyOps(list, (args) => this.#opTargets(args))\n }\n\n // Resolve an op's targets: \"@root\" is this element; a selector resolves\n // WITHIN this root and excludes nested reactive roots' subtrees (issue #15\n // semantics — the same nearest-root ownership check the field walk uses);\n // global: true opts a single op out to document-wide.\n #opTargets(args) {\n const to = args.to\n if (to === \"@root\") return [this.element]\n if (typeof to !== \"string\" || to === \"\") return []\n if (args.global) return [...document.querySelectorAll(to)]\n return [...this.element.querySelectorAll(to)].filter((el) => this.#ownsField(el))\n }\n\n // Whether a click-bound checkbox/radio trigger should keep its NATIVE flip\n // (issue #98). `checked: :keep` means \"let the control flip now\": the\n // unconditional preventDefault is exactly what suppresses that flip until the\n // morph, so we skip it — but ONLY for a checkbox/radio (a bare toggle click\n // has no form-navigation default to lose). Any other element (a button) keeps\n // preventDefault so an in-form submit can't navigate.\n #keepsNativeToggle(optimistic, target) {\n if (optimistic?.checked !== \"keep\") return false\n const type = target?.type\n return type === \"checkbox\" || type === \"radio\"\n }\n\n // Apply the optimistic hint (issue #98) to its targets NOW and return the\n // INVERSE — the exact ops to replay on failure. Cosmetic only: class ops and\n // hidden, applied to the trigger by default or to a `to:` selector scoped to\n // the root. `checked: :keep` records the trigger's post-flip state so a\n // failure snaps the native control back; it applies no DOM change itself (the\n // browser already flipped it). Returns null when there is nothing to do, so\n // the success/failure paths can cheaply skip.\n #applyOptimistic(optimistic, trigger) {\n if (!optimistic) return null\n\n // Class + hidden ops share one target set (trigger, or the `to:` selector).\n const targets = this.#optimisticTargets(optimistic, trigger)\n const undo = []\n for (const el of targets) {\n if (optimistic.add_class) {\n // Undo only the classes this op ACTUALLY added — a class already present\n // was not our change, so reverting it would strip a class the element\n // legitimately had (the add was a no-op). Capture the real delta now.\n const added = optimistic.add_class.filter((c) => !el.classList.contains(c))\n el.classList.add(...added)\n if (added.length) undo.push(() => el.classList.remove(...added))\n }\n if (optimistic.remove_class) {\n // Symmetric: undo only the classes actually removed — one already absent\n // wasn't our change, so re-adding it would introduce a class that wasn't\n // there before.\n const removed = optimistic.remove_class.filter((c) => el.classList.contains(c))\n el.classList.remove(...removed)\n if (removed.length) undo.push(() => el.classList.add(...removed))\n }\n if (optimistic.toggle_class) {\n // toggle_class is its own inverse regardless of prior state — toggling\n // the same classes back exactly restores it, no delta tracking needed.\n optimistic.toggle_class.forEach((c) => el.classList.toggle(c))\n undo.push(() => optimistic.toggle_class.forEach((c) => el.classList.toggle(c)))\n }\n if (optimistic.hide) {\n el.hidden = true\n undo.push(() => (el.hidden = false))\n }\n }\n\n // checked: :keep — the native flip already happened on the trigger; record\n // the inverse (flip it back) so a failure reverts the control's state.\n if (optimistic.checked === \"keep\" && trigger && \"checked\" in trigger) {\n const flipped = trigger.checked\n undo.push(() => (trigger.checked = !flipped))\n }\n\n return undo.length ? undo : null\n }\n\n // Replay the recorded inverse ops on failure (issue #98), guarded by\n // isConnected: a plain (non-morph) replace can detach this subtree before the\n // failure lands, and reverting a stale/detached node is pointless (it's gone)\n // — so a disconnected root skips the revert entirely. On success NOTHING calls\n // this: the server re-render overwrites the hint, or (reply.remove /\n // streams-only) the hint is deliberately left standing.\n #revertOptimistic(inverse) {\n if (!inverse) return\n if (!this.element.isConnected) return\n for (const undo of inverse) undo()\n }\n\n // The elements an optimistic class/hidden hint applies to: the `to:` selector\n // (resolved like an op target — \"@root\" is the root, a selector is scoped to\n // this root's owned matches) or, with no `to:`, the trigger itself.\n #optimisticTargets(optimistic, trigger) {\n if (optimistic.to == null) return trigger ? [trigger] : []\n return this.#opTargets({ to: optimistic.to })\n }\n\n // Apply the loading state for THIS enqueue (issue #99) and return a `settle`\n // closure that undoes exactly this enqueue's contribution when the round trip\n // finishes (success OR any failure). Everything is refcounted so overlapping\n // enqueues never clobber: A's settle can't clear busy while B is still pending.\n //\n // Two layers:\n // 1. The ALWAYS-ON busy vocabulary (fires for every action, no hint needed):\n // data-reactive-busy=\"<action>\" on the trigger and the root (a\n // space-separated, per-action refcounted set), aria-busy on the root (a\n // pending counter), and data-reactive-busy on any busy_on element scoped\n // to this action. Apps style a spinner with pure CSS and zero Ruby.\n // 2. The loading HINT (only when loading:/disable_with: was declared):\n // disable the trigger, add a loading class (to the trigger or a `to:`\n // target), swap its text. These apply at ENQUEUE — never during a debounce\n // quiet period — so a debounced input is not disabled mid-typing.\n #applyLoading(action, trigger, loading) {\n this.#markBusy(action, trigger)\n const restoreHint = this.#applyLoadingHint(action, trigger, loading)\n\n let settled = false\n return () => {\n if (settled) return // one settle per enqueue, even if called twice\n settled = true\n this.#unmarkBusy(action, trigger)\n restoreHint()\n }\n }\n\n // Layer 1 — the always-on busy markers. Trigger + root carry the action token;\n // the root's counter drives aria-busy; busy_on elements scoped to this action\n // light up. Refcounts (#busyActions, #busyPending) so overlapping requests\n // don't clear each other.\n #markBusy(action, trigger) {\n this.#setBusyToken(trigger, action, +1)\n this.#setBusyToken(this.element, action, +1)\n\n this.#busyActions.set(action, (this.#busyActions.get(action) ?? 0) + 1)\n if (this.#busyPending++ === 0) this.element.setAttribute(\"aria-busy\", \"true\")\n\n for (const el of this.#busyOnTargets(action)) this.#setBusyToken(el, action, +1)\n }\n\n #unmarkBusy(action, trigger) {\n this.#setBusyToken(trigger, action, -1)\n this.#setBusyToken(this.element, action, -1)\n\n const count = (this.#busyActions.get(action) ?? 1) - 1\n if (count <= 0) this.#busyActions.delete(action)\n else this.#busyActions.set(action, count)\n\n if (--this.#busyPending <= 0) {\n this.#busyPending = 0\n this.element.removeAttribute(\"aria-busy\")\n }\n\n for (const el of this.#busyOnTargets(action)) this.#setBusyToken(el, action, -1)\n }\n\n // Add (+1) or remove (-1) `action` from an element's space-separated\n // data-reactive-busy token set, refcounted PER ELEMENT+ACTION so two queued\n // requests of the same action on the same element don't drop the token early\n // (and two DIFFERENT actions both keep their token — the set never clobbers).\n // The attribute is removed only when the set empties. No-op on a nullish/\n // detached element (a morph may have replaced the trigger before settle).\n #setBusyToken(el, action, delta) {\n if (!el || typeof el.getAttribute !== \"function\") return\n\n const counts = (this.#busyTokenCounts.get(el) ?? new Map())\n const next = (counts.get(action) ?? 0) + delta\n if (next <= 0) counts.delete(action)\n else counts.set(action, next)\n\n if (counts.size === 0) {\n this.#busyTokenCounts.delete(el)\n el.removeAttribute(\"data-reactive-busy\")\n return\n }\n this.#busyTokenCounts.set(el, counts)\n el.setAttribute(\"data-reactive-busy\", [...counts.keys()].join(\" \"))\n }\n\n // busy_on elements scoped to THIS action, owned by this root (not a nested\n // reactive root's, issue #15). data-reactive-busy-on=\"<action>\" is the marker\n // busy_on(:action) emits.\n #busyOnTargets(action) {\n const nodes = this.element.querySelectorAll?.(\"[data-reactive-busy-on]\") ?? []\n return [...nodes].filter(\n (el) => el.getAttribute(\"data-reactive-busy-on\") === action && this.#ownsField(el),\n )\n }\n\n // Layer 2 — the loading HINT (disable + class + text). Snapshots the trigger's\n // ORIGINAL disabled/text/classes on the FIRST enqueue for that trigger\n // (refcounted so an overlapping enqueue never snapshots the already-swapped\n // \"Saving…\" as the original), applies the swap, and returns a restore closure.\n // With no hint, returns a no-op restore (the always-on busy markers still ran).\n #applyLoadingHint(action, trigger, loading) {\n if (!loading || !trigger) return () => {}\n\n const classTargets = this.#loadingTargets(loading, trigger)\n const classes = Array.isArray(loading.class) ? loading.class : []\n const addedByTarget = []\n for (const el of classTargets) {\n const added = classes.filter((c) => !el.classList.contains(c))\n el.classList.add(...added)\n if (added.length) addedByTarget.push([el, added])\n }\n\n // Snapshot disabled/text ONCE per trigger (refcounted). A second overlapping\n // enqueue increments the count but does NOT re-snapshot — so the recorded\n // \"original\" is the true pre-loading state, never the swapped label.\n const snap = this.#loadingSnapshots.get(trigger)\n if (snap) {\n snap.count++\n } else if (loading.disable || loading.text != null) {\n this.#loadingSnapshots.set(trigger, {\n count: 1,\n disabled: trigger.disabled,\n text: trigger.textContent,\n hadText: loading.text != null,\n })\n }\n\n if (loading.disable) trigger.disabled = true\n if (loading.text != null) trigger.textContent = loading.text\n\n return () => {\n for (const [el, added] of addedByTarget) if (el.isConnected) el.classList.remove(...added)\n this.#restoreLoadingSnapshot(trigger, loading)\n }\n }\n\n // Restore the trigger's disabled/text from its snapshot when the LAST enqueue\n // for that trigger settles (refcount → 0). GUARDED: skip a disconnected\n // trigger (a plain replace detached it — the node is gone), and do NOT restore\n // the text if it no longer equals what we swapped IN (a morph rendered a new\n // server label — clobbering it with the old text would fight server truth).\n #restoreLoadingSnapshot(trigger, loading) {\n const snap = this.#loadingSnapshots.get(trigger)\n if (!snap) return\n if (--snap.count > 0) return // another enqueue for this trigger is still pending\n this.#loadingSnapshots.delete(trigger)\n\n if (!trigger.isConnected) return // detached — nothing to restore\n\n if (loading.disable) trigger.disabled = snap.disabled\n // Only restore the label if the trigger still shows OUR swapped text; a\n // changed textContent means the server morph relabeled it — leave it.\n if (snap.hadText && trigger.textContent === loading.text) trigger.textContent = snap.text\n }\n\n // The elements a loading class applies to: the `to:` selector (resolved like\n // an op target — \"@root\" is the root, a selector is scoped to this root's\n // owned matches) or, with no `to:`, the trigger itself.\n #loadingTargets(loading, trigger) {\n if (loading.to == null) return trigger ? [trigger] : []\n return this.#opTargets({ to: loading.to })\n }\n\n // The action path comes from a <meta> tag that is fixed for the page's life,\n // so resolve it once per controller and cache it — avoids a querySelector on\n // every dispatch (this runs on the request hot path, once per click/keystroke\n // round trip). Cached on the instance, so a fresh connect() (after a Turbo\n // navigation swaps the element) re-reads it.\n #actionPath() {\n return (this.#actionPathCache ??=\n document.querySelector('meta[name=\"phlex-reactive-action-path\"]')?.content ||\n \"/reactive/actions\")\n }\n\n // The per-request timeout in ms (issue #101), from a page-stable\n // <meta name=\"phlex-reactive-timeout\"> (default 30000). Cached per-controller\n // like the action path. Parsed defensively: a missing/blank/non-positive/NaN\n // value falls back to the default, so a typo'd meta can never disable the\n // timeout (which would reintroduce the wedged-queue bug) or set a zero/negative\n // window that aborts instantly.\n #timeoutMs() {\n if (this.#timeoutMsCache != null) return this.#timeoutMsCache\n const raw = document.querySelector('meta[name=\"phlex-reactive-timeout\"]')?.content\n const ms = Number(raw)\n return (this.#timeoutMsCache = Number.isFinite(ms) && ms > 0 ? ms : 30000)\n }\n\n // CSRF token and connection id are read LIVE (not cached) on purpose: Rails\n // can rotate the CSRF token, and the pgbus connection id changes on an SSE\n // reconnect — caching either would send a stale value. A single querySelector\n // per request is cheap next to the round trip itself.\n #csrfToken() {\n return document.querySelector('meta[name=\"csrf-token\"]')?.content ?? \"\"\n }\n\n // The pgbus SSE connection id, if the page is subscribed to a stream. pgbus\n // reflects it onto the <pgbus-stream-source connection-id=\"…\"> element (and\n // apps may mirror it to <meta name=\"pgbus-connection-id\">). Returns null\n // when not present (e.g. no pgbus, or no active subscription) — the header\n // is then simply omitted.\n #connectionId() {\n return (\n document.querySelector(\"pgbus-stream-source[connection-id]\")?.getAttribute(\"connection-id\") ||\n document.querySelector('meta[name=\"pgbus-connection-id\"]')?.content ||\n null\n )\n }\n}\n"
5
+ "import { Controller } from \"@hotwired/stimulus\"\n// Import the BARE specifier the engine already pins (phlex/reactive/confirm),\n// NOT a relative \"./confirm.js\" (issue #57). Under importmap-rails + Propshaft\n// the controller is served at its DIGESTED url; a relative sibling import is\n// left untouched (Propshaft rewrites only RAILS_ASSET_URL(...), and the import\n// map resolves ONLY bare specifiers), so \"./confirm.js\" resolves against the\n// digested controller url → an undigested /assets/.../confirm.js that 404s, and\n// the throwing import takes down every Stimulus controller on the page. The\n// bare specifier resolves to the digested asset through the import map, and\n// bundlers/bun resolve it the same way they already resolve\n// \"phlex/reactive/reactive_controller\" (see tsconfig.json paths for the tests).\nimport { confirmResolver } from \"phlex/reactive/confirm\"\n// Client-side computes (data bindings): the reducer registry behind\n// reactive_compute. Bare specifier for the same import-map reason as confirm.\nimport { computeReducer } from \"phlex/reactive/compute\"\n\n// The ONE generic controller behind every reactive Phlex component. It\n// replaces the per-feature Stimulus controllers you'd otherwise hand-write\n// for interactive components. A component declares its actions in Ruby (via\n// Phlex::Reactive::Component); this controller binds DOM events to a single\n// HTTP round trip and lets Turbo apply the re-rendered component back in\n// (replace by default; method=\"morph\" — Response.morph — preserves focus).\n//\n// Wire format (client -> server), POST <action path>, turbo-stream Accept:\n// { token: \"<signed identity>\", act: \"<action>\", params: {...} } (JSON)\n// (`act`, not `action`: `action` is a reserved Rails routing param.)\n// The token is a MessageVerifier-signed { component, gid } — NO state is sent.\n// When the root holds a chosen <input type=\"file\">, the SAME payload is sent as\n// multipart FormData instead (token/act flat, params bracketed, files appended)\n// so an upload reaches the action (issue #34) — only the encoding differs.\n// The response is a <turbo-stream> that replaces the component by its id.\n//\n// Server -> client live updates use the SAME element id, pushed over the\n// stream transport (pgbus SSE / Action Cable) via the Streamable\n// .broadcast_* methods — so a click and a background broadcast converge on\n// one re-render unit.\n//\n// Custom turbo-stream action: the server tells the actor to full-navigate\n// (e.g. the record's slug changed and the current URL is now dead). It rides a\n// 200 turbo-stream — NOT an HTTP 3xx — so it never trips the response.redirected\n// bail below (which still correctly catches real auth/CSRF redirects). Registered\n// once on the Turbo global (no @hotwired/turbo import — the gem uses window.Turbo\n// everywhere, and a named import is unreliable under importmap/esbuild).\nexport function registerReactiveVisit() {\n const actions = window.Turbo?.StreamActions\n if (!actions || actions[\"reactive:visit\"]) return\n actions[\"reactive:visit\"] = function () {\n const url = this.getAttribute(\"data-url\")\n if (url) window.Turbo.visit(url, { action: \"advance\" })\n }\n}\n\n// Custom turbo-stream action: a TOKEN-ONLY refresh (issue #30). A partial\n// update (Response.streams / reply.streams) re-renders only PART of a component\n// — so there's no full-self replace to carry the next signed token. The server\n// instead emits `<turbo-stream action=\"reactive:token\" target=\"<id>\"\n// data-reactive-token-value=\"<fresh>\">`. #perform's #extractToken already reads\n// the token out of the response body for the NEXT queued request; this handler\n// keeps the DOM in sync too, writing the attribute onto the root element so the\n// `tokenValue` fallback stays fresh. It's a pure attribute set — no node is\n// replaced — so a focused <input> + caret survive (the whole point: update a\n// total cell without tearing down the field the user is typing in).\nexport function registerReactiveToken() {\n const actions = window.Turbo?.StreamActions\n if (!actions || actions[\"reactive:token\"]) return\n actions[\"reactive:token\"] = function () {\n const token = this.getAttribute(\"data-reactive-token-value\")\n const target = this.getAttribute(\"target\")\n if (!token || !target) return\n const el = document.getElementById(target)\n // Stimulus reads the token via the `token` value -> data-reactive-token-value.\n if (el) el.setAttribute(\"data-reactive-token-value\", token)\n }\n}\n\n// Custom turbo-stream action: SERVER-PUSHED client DOM ops (issue #97). The\n// server-side sibling of on_client's runOps — a reply (reply.<verb>.js(ops)) or\n// a broadcast (Streamable.broadcast_js_to) emits\n//\n// <turbo-stream action=\"reactive:js\" target=\"<optional root id>\"\n// data-reactive-ops=\"[[op, args], ...]\"></turbo-stream>\n//\n// and Turbo invokes this handler with `this` bound to that <turbo-stream>\n// element. It runs the ops through the SAME frozen CLIENT_OPS whitelist as\n// runOps (client-side default-deny — an unknown op warns + is skipped), so a\n// forged/stale ops attr can never break the page or execute anything off the\n// vocabulary. NO token, NO fetch — a pure local DOM mutation.\n//\n// `target` (optional, an element id) scopes op resolution to that root: \"@root\"\n// resolves to the target element itself and a selector resolves WITHIN it.\n// Without a target, ops resolve document-wide (a broadcast op like\n// add_class(\"#bell\", ...) that isn't anchored to one component). The op stream\n// is emitted AFTER all render streams in the reply (the endpoint appends it\n// last), so focus(\"[name=next]\") sees the freshly morphed DOM — Turbo applies\n// streams in document order.\nexport function registerReactiveJs() {\n const actions = window.Turbo?.StreamActions\n if (!actions || actions[\"reactive:js\"]) return\n actions[\"reactive:js\"] = function () {\n const list = parseOps(this.getAttribute(\"data-reactive-ops\"))\n if (!list.length) return\n const targetId = this.getAttribute(\"target\")\n // With a target: scope to that element (missing → no-op). Without: document.\n const root = targetId ? document.getElementById(targetId) : null\n if (targetId && !root) return\n applyOps(list, (args) => streamOpTargets(args, root))\n }\n}\n\n// --- Deferred reply segments (issue #165) ----------------------------------\n// The client half of reply.defer: the server's reply carries a\n// `<turbo-stream action=\"reactive:defer\" target=\"<id>\">` directive and the\n// real render reaches the SAME actor later — via a parallel fetch (pull) or a\n// pgbus one-shot stream (push). Everything here is MODULE-level, deliberately\n// OFF the per-controller request queue: the whole point is that the expensive\n// segment never blocks the actor's next action.\n//\n// Supersession is the correctness core: pendingDefers keys one in-flight\n// delivery per target id. A newer directive for the same target aborts the\n// older fetch (or removes the older stream source), and an arrival applies\n// ONLY while its entry is still current — so a fast typist's debounced\n// keystrokes can never paint stale totals over fresh ones.\nconst pendingDefers = new Map()\n\n// Test seam: clear the module-level registry between unit tests. Also resets\n// the one-shot settle-listener guard so a test's fresh document re-registers\n// the turbo:before-stream-render settler.\nexport function resetReactiveDefers() {\n pendingDefers.clear()\n deferStreamSettleRegistered = false\n}\n\n// Test seam: the `via` of a target's pending defer entry (or undefined) — lets\n// tests assert an entry was SETTLED (dropped) on arrival without exposing the\n// Map. Not used by the runtime.\nexport function pendingDeferVia(targetId) {\n return pendingDefers.get(targetId)?.via\n}\n\nlet deferStreamSettleRegistered = false\n\nexport function registerReactiveDefer() {\n const actions = window.Turbo?.StreamActions\n if (!actions || actions[\"reactive:defer\"]) return\n actions[\"reactive:defer\"] = function () {\n const target = this.getAttribute(\"target\")\n if (!target) return\n if (this.getAttribute(\"data-reactive-defer-via\") === \"stream\") {\n startStreamDefer(target, this)\n return\n }\n const token = this.getAttribute(\"data-reactive-defer-token\")\n if (!token) return\n startFetchDefer(target, token)\n }\n\n // Settle a STREAM-lane pendingDefers entry when its arrival lands: the job's\n // broadcast is a turbo-stream that replaces the target (and removes the\n // source). A document-level turbo:before-stream-render hook drops the Map\n // entry for a stream target the moment a stream renders against it — so the\n // entry never outlives the delivery (the fetch lane settles inline; the\n // stream lane's arrival is a broadcast this controller doesn't await, so it\n // needs this hook). Registered once; a no-op without document.\n if (!deferStreamSettleRegistered && typeof document !== \"undefined\" && document.addEventListener) {\n deferStreamSettleRegistered = true\n document.addEventListener(\"turbo:before-stream-render\", settleStreamDeferOnRender)\n }\n}\n\n// Drop a stream-lane pendingDefers entry when a turbo-stream renders against\n// its target id (the job's replace) OR removes its source element. Keyed by the\n// stream's target so an unrelated stream never settles a defer. Pure Map\n// cleanup — the DOM apply is Turbo's; this only releases our bookkeeping.\nfunction settleStreamDeferOnRender(event) {\n const streamEl = event.target\n const target = streamEl?.getAttribute?.(\"target\")\n if (!target) return\n // The arrival replaces #<target>; the source removal targets\n // reactive-defer-src-<target>. Either signals the stream delivered.\n const targetId = target.startsWith(\"reactive-defer-src-\")\n ? target.slice(\"reactive-defer-src-\".length)\n : target\n const entry = pendingDefers.get(targetId)\n if (entry?.via === \"stream\") pendingDefers.delete(targetId)\n}\n\n// The pull lane: mark the target pending and POST the signed defer token to\n// the defer endpoint, in parallel with everything else the page is doing.\nfunction startFetchDefer(targetId, token) {\n const el = document.getElementById(targetId)\n if (!el) {\n console.warn(`[phlex-reactive] reactive:defer target #${targetId} is not on the page — skipped`)\n return\n }\n supersedeDefer(targetId)\n markDeferPending(el)\n const entry = { via: \"fetch\", abort: new AbortController(), timedOut: false }\n pendingDefers.set(targetId, entry)\n performDeferFetch(targetId, entry, token)\n}\n\n// The push lane: subscribe a <pgbus-stream-source> to the server-signed\n// one-shot stream. Arrival + teardown need no client logic — the job's\n// broadcast carries the replace AND a remove of this source element (its\n// disconnectedCallback closes the SSE connection). since-id=0 on a fresh key\n// replays a broadcast that beat the subscription (the durable-lane guarantee).\nfunction startStreamDefer(targetId, directive) {\n const el = document.getElementById(targetId)\n if (!el) {\n console.warn(`[phlex-reactive] reactive:defer target #${targetId} is not on the page — skipped`)\n return\n }\n const src = directive.getAttribute(\"data-reactive-defer-src\")\n if (!src) return\n if (!globalThis.customElements?.get?.(\"pgbus-stream-source\")) {\n // The server chose push on server-side capability, but this page has no\n // pgbus client. Degrade to the fetch lane using the fallback token the push\n // directive carries — rather than dead-end the shimmer. No token (an app on\n // a bespoke transport) is a loud no-op.\n const fallbackToken = directive.getAttribute(\"data-reactive-defer-token\")\n if (fallbackToken) {\n startFetchDefer(targetId, fallbackToken)\n return\n }\n console.error(\n \"[phlex-reactive] reactive:defer via=stream but <pgbus-stream-source> is not registered \" +\n \"and no fallback token was provided — is the pgbus client loaded on this page?\",\n )\n return\n }\n supersedeDefer(targetId)\n markDeferPending(el)\n const source = document.createElement(\"pgbus-stream-source\")\n // Deterministic id: the JOB's broadcast removes it by this exact id — the\n // subscription tears itself down with the payload it delivered.\n source.id = deferSourceId(targetId)\n source.setAttribute(\"src\", src)\n source.setAttribute(\"since-id\", directive.getAttribute(\"data-reactive-defer-since-id\") ?? \"0\")\n source.setAttribute(\"hidden\", \"\")\n document.body.appendChild(source)\n // Record only { via } — NOT a strong ref to the source element. The job's\n // broadcast removes the source by its deterministic id (its\n // disconnectedCallback closes the SSE), so holding srcEl here would pin the\n // detached node in this module-level Map forever (a leak). Supersession\n // re-finds the element by id instead. The entry is dropped on\n // supersession or when the arriving broadcast replaces the target (a\n // turbo:before-stream-render hook, below).\n pendingDefers.set(targetId, { via: \"stream\" })\n}\n\n// The deterministic id of a target's one-shot <pgbus-stream-source>.\nfunction deferSourceId(targetId) {\n return `reactive-defer-src-${targetId}`\n}\n\nasync function performDeferFetch(targetId, entry, token) {\n // Bound the wait like the action fetch (issue #101) — a hung defer must not\n // shimmer forever. A manual timer (not AbortSignal.timeout) so the catch can\n // tell a TIMEOUT (fail loudly) from a SUPERSEDED abort (stay silent).\n const timer = setTimeout(() => {\n entry.timedOut = true\n entry.abort.abort()\n }, deferTimeoutMs())\n\n // The timeout is cleared ONLY after the body is fully read (below), not the\n // moment headers arrive — a server that streams headers then stalls the body\n // must still abort, or the shimmer hangs forever (the abort signal covers the\n // whole fetch + body read, mirroring #perform's AbortSignal.timeout).\n let response\n try {\n response = await fetch(deferPath(), {\n method: \"POST\",\n headers: {\n Accept: \"text/vnd.turbo-stream.html\",\n \"Content-Type\": \"application/json\",\n \"X-CSRF-Token\": deferCsrfToken(),\n },\n body: JSON.stringify({ token }),\n credentials: \"same-origin\",\n signal: entry.abort.signal,\n })\n } catch (error) {\n clearTimeout(timer)\n if (pendingDefers.get(targetId) !== entry) return // superseded — silent\n console.error(\"[phlex-reactive] deferred render failed\", error)\n failDefer(targetId, token)\n return\n }\n if (pendingDefers.get(targetId) !== entry) {\n clearTimeout(timer)\n return // superseded mid-flight\n }\n\n if (response.status === 204) {\n clearTimeout(timer)\n // render? false — keep the current content, just clear the pending state.\n settleDefer(targetId)\n return\n }\n if (!response.ok) {\n clearTimeout(timer)\n console.error(`[phlex-reactive] deferred render failed: HTTP ${response.status}`)\n failDefer(targetId, token, response.status)\n return\n }\n\n let html\n try {\n html = await response.text()\n } catch (error) {\n clearTimeout(timer)\n if (pendingDefers.get(targetId) !== entry) return\n console.error(\"[phlex-reactive] deferred render failed reading the body\", error)\n failDefer(targetId, token)\n return\n }\n clearTimeout(timer)\n if (pendingDefers.get(targetId) !== entry) return // superseded during read\n\n settleDefer(targetId)\n // A normal replace/morph of the target — the fresh root carries no pending\n // markers and a fresh action token, so the component lands interactive.\n window.Turbo.renderStreamMessage(html)\n}\n\n// Abort/unsubscribe whatever delivery is in flight for this target. The\n// deleted entry makes every late arrival fail its identity check — stale\n// content can never paint.\nfunction supersedeDefer(targetId) {\n const existing = pendingDefers.get(targetId)\n if (!existing) return\n pendingDefers.delete(targetId)\n if (existing.via === \"fetch\") existing.abort.abort()\n // Stream lane: re-find the old source by its deterministic id and remove it\n // (unsubscribe) — we deliberately don't hold a strong ref to the detached\n // node. Its disconnectedCallback closes the SSE.\n else document.getElementById(deferSourceId(targetId))?.remove?.()\n}\n\nfunction markDeferPending(el) {\n el.setAttribute(\"data-reactive-defer-pending\", \"true\")\n el.setAttribute(\"aria-busy\", \"true\")\n}\n\nfunction clearDeferPending(el) {\n el.removeAttribute(\"data-reactive-defer-pending\")\n el.removeAttribute(\"aria-busy\")\n}\n\n// Success/204: drop the registry entry, clear pending, and clear any prior\n// defer failure marker (recovery resets error-driven CSS, issue #100 style).\nfunction settleDefer(targetId) {\n pendingDefers.delete(targetId)\n const el = document.getElementById(targetId)\n if (!el) return\n clearDeferPending(el)\n el.removeAttribute(\"data-reactive-error\")\n}\n\n// Failure: clear pending (the shimmer must not lie), mark the root\n// (data-reactive-error=\"defer\" — style it in pure CSS), and emit a bubbling\n// reactive:error whose retry() re-enters the defer fetch with the SAME token\n// (still valid inside the TTL; an expired token 400s into this same path).\nfunction failDefer(targetId, token, status) {\n pendingDefers.delete(targetId)\n const el = document.getElementById(targetId)\n if (!el) return\n clearDeferPending(el)\n el.setAttribute(\"data-reactive-error\", \"defer\")\n const retry = () => {\n const fresh = document.getElementById(targetId)\n if (!fresh) {\n console.warn(\"[phlex-reactive] defer retry() ignored — the target left the DOM\")\n return\n }\n fresh.removeAttribute(\"data-reactive-error\")\n startFetchDefer(targetId, token)\n }\n el.dispatchEvent(\n new CustomEvent(\"reactive:error\", {\n bubbles: true,\n composed: true,\n detail: { kind: \"defer\", target: targetId, status, retry },\n }),\n )\n}\n\nfunction deferPath() {\n return document.querySelector('meta[name=\"phlex-reactive-defer-path\"]')?.content || \"/reactive/defer\"\n}\n\n// CSRF is read LIVE per request (Rails can rotate it) — same contract as the\n// controller's #csrfToken.\nfunction deferCsrfToken() {\n return document.querySelector('meta[name=\"csrf-token\"]')?.content ?? \"\"\n}\n\n// Same page-stable meta + default as the controller's #timeoutMs (issue #101),\n// parsed defensively so a typo'd meta can never disable the bound.\nfunction deferTimeoutMs() {\n const raw = document.querySelector('meta[name=\"phlex-reactive-timeout\"]')?.content\n const ms = Number(raw)\n return Number.isFinite(ms) && ms > 0 ? ms : 30000\n}\n\n// Document-level self-dismissing flashes (issue #100). A flash rendered with\n// dismiss_after: carries data-reactive-dismiss-after=\"<ms>\"; after the timeout\n// it removes itself. This is deliberately NOT a Stimulus controller — the flash\n// container is a plain host-app div (Response#flash appends into it) with no\n// controller attached, so nothing would honor the attr. A document-level scan\n// on turbo:before-stream-render (which fires for EVERY <turbo-stream> render —\n// a reply AND a broadcast) schedules removal for any newly-arrived dismissing\n// flash. Each is marked data-reactive-dismiss-scheduled so re-scans (a later\n// stream render) never double-schedule the same node. Registered once; the\n// guard flag makes a second call a no-op (bun imports the module once per run).\nlet dismissRegistered = false\nexport function registerReactiveDismiss() {\n if (dismissRegistered) return\n if (typeof document === \"undefined\" || !document.addEventListener) return\n dismissRegistered = true\n // turbo:before-stream-render fires BEFORE the stream is applied — and Turbo\n // then does `await nextRepaint(); await event.detail.render(this)`, so a bare\n // setTimeout(0) can run BEFORE the node is inserted (observed under Falcon).\n // WRAP event.detail.render instead: run Turbo's own render, then scan once it\n // has resolved — timing-independent and correct on every server. The event\n // fires for EVERY <turbo-stream> (a reply AND a broadcast), so both delivery\n // paths self-clean. detail.render may be absent on exotic streams — guard it.\n document.addEventListener(\"turbo:before-stream-render\", wrapStreamRenderForDismiss)\n}\n\n// Chain the dismissing-flash scan after Turbo's own stream render resolves, so\n// the scan sees the freshly-inserted node. Idempotent per event (marks\n// detail.render as already-wrapped) and defensive if detail/render is missing.\nfunction wrapStreamRenderForDismiss(event) {\n const detail = event.detail\n const original = detail?.render\n if (typeof original !== \"function\" || original.__reactiveDismissWrapped) {\n // No render to wrap (or already wrapped) — fall back to a post-repaint scan.\n if (typeof requestAnimationFrame === \"function\") requestAnimationFrame(scheduleReactiveDismissals)\n else setTimeout(scheduleReactiveDismissals, 0)\n return\n }\n const wrapped = async (streamElement) => {\n await original(streamElement)\n scheduleReactiveDismissals()\n }\n wrapped.__reactiveDismissWrapped = true\n detail.render = wrapped\n}\n\n// Scan for un-scheduled dismissing flashes and schedule each one's removal.\n// Kept a module function so the scan logic is testable and re-run on every\n// stream render.\nfunction scheduleReactiveDismissals() {\n const flashes = document.querySelectorAll(\"[data-reactive-dismiss-after]\")\n for (const el of flashes) {\n if (el.hasAttribute(\"data-reactive-dismiss-scheduled\")) continue\n const ms = Number(el.getAttribute(\"data-reactive-dismiss-after\"))\n if (!Number.isFinite(ms) || ms <= 0) continue\n el.setAttribute(\"data-reactive-dismiss-scheduled\", \"\")\n setTimeout(() => el.remove(), ms)\n }\n}\n\n// Test seam: reset the one-time registration guard so a fresh document stub in\n// the next test registers its own listener (bun runs all specs in one process).\nexport function __resetReactiveDismissForTest() {\n dismissRegistered = false\n}\n\n// Offline CSS hook (issue #101). Mirror data-reactive-offline on\n// document.documentElement from navigator.onLine, kept in sync by the window\n// online/offline events — so an app can dim a save button or show a banner with\n// PURE CSS and zero JS ([data-reactive-offline] .save { pointer-events: none }).\n// Guarded on window (needed for addEventListener AND navigator) so importing the\n// module in a non-browser (bun test) context is a no-op, and registered once\n// (the online/offline listeners are NOT {once}, so a second registerReactiveActions\n// call must not stack duplicates) — mirroring the dismiss guard + reset seam.\nlet offlineRegistered = false\nexport function registerReactiveOffline() {\n if (offlineRegistered) return\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n if (typeof window.addEventListener !== \"function\") return\n offlineRegistered = true\n // toggleAttribute(name, force) writes data-reactive-offline=\"\" (a bare boolean\n // attr the [data-reactive-offline] selector matches) or removes it — never the\n // \"true\" string. navigator.onLine === false is the reliable direction (a false\n // \"online\" is spec-permitted but rare, and this is only a presentational hook —\n // the authoritative offline signal is the #perform gate, not this attribute).\n // Fully defensive: a missing documentElement/toggleAttribute/navigator degrades\n // to a no-op — a presentational hook must NEVER throw during bootstrap.\n const sync = () => {\n const root = document.documentElement\n if (typeof root?.toggleAttribute !== \"function\") return\n root.toggleAttribute(\"data-reactive-offline\", globalThis.navigator?.onLine === false)\n }\n sync() // seed synchronously so first paint is correct\n window.addEventListener(\"online\", sync)\n window.addEventListener(\"offline\", sync)\n}\n\nexport function __resetReactiveOfflineForTest() {\n offlineRegistered = false\n}\n\n// Latency simulator dev aid (issue #102). On localhost the click→morph round\n// trip is ~5ms, so the pending/loading/optimistic affordances (aria-busy,\n// disable_with, busy_on, optimistic hints) flash by too fast to see while\n// developing or demoing them — the reason LiveView ships enableLatencySim(ms).\n//\n// enableLatencySim(ms) persists the delay to sessionStorage (session-scoped, so\n// it clears when the tab closes — never a config you forget you left on);\n// #perform reads it right before the fetch and awaits setTimeout(ms), stretching\n// the already-set busy window to something visible. disableLatencySim() clears\n// it. NAMED exports (the setConfirmResolver precedent) — but importmap module\n// exports are unreachable from the browser console, so registerReactiveActions\n// ALSO attaches these to window.PhlexReactive, and ONLY when the app opts in with\n// <meta name=\"phlex-reactive-env\" content=\"development\"> (see #attachLatencyHandle).\nexport const LATENCY_KEY = \"phlex-reactive:latency\"\n\n// One-time \"sim active\" banner guard (module-level, mirroring offlineRegistered):\n// #maybeSimulateLatency warns ONCE while the sim is on, not once per request.\nlet latencyBannerShown = false\n\nexport function enableLatencySim(ms) {\n if (typeof sessionStorage === \"undefined\") return\n sessionStorage.setItem(LATENCY_KEY, String(ms))\n}\n\nexport function disableLatencySim() {\n if (typeof sessionStorage === \"undefined\") return\n sessionStorage.removeItem(LATENCY_KEY)\n // Re-arm the one-time \"sim active\" banner: turning the sim OFF is the lifecycle\n // boundary, so a later enableLatencySim() in the same session re-announces that\n // the sim is on (otherwise the guard would stay set across an off→on cycle and\n // swallow the banner). Matches the __resetReactiveLatencyForTest seam.\n latencyBannerShown = false\n}\n\n// The dev gate. importmap module exports aren't reachable from the DevTools\n// console, so we expose the two functions on a window handle — but ONLY when the\n// app authored <meta name=\"phlex-reactive-env\" content=\"development\">. There is\n// NO engine-emitted meta (the engine can't inject into the host layout); the\n// install generator ships the snippet commented. Without the meta: no global\n// handle at all, and #perform short-circuits on the null sessionStorage read —\n// zero production surface. The `?.content` chain is fully defensive (a stubbed\n// document with no querySelector, a missing meta) so bootstrap never throws.\nfunction attachLatencyHandle() {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n const env = document.querySelector?.('meta[name=\"phlex-reactive-env\"]')?.content\n if (env !== \"development\") return\n window.PhlexReactive = { enableLatencySim, disableLatencySim }\n}\n\n// Test seam: forget the one-time active-sim banner so the next test re-warns.\nexport function __resetReactiveLatencyForTest() {\n latencyBannerShown = false\n}\n\nexport function registerReactiveActions() {\n registerReactiveVisit()\n registerReactiveToken()\n registerReactiveJs()\n registerReactiveDefer()\n registerReactiveDismiss()\n registerReactiveOffline()\n attachLatencyHandle()\n}\n\n// Escape a DOM id for safe interpolation into a RegExp (an id can legally contain\n// regex metacharacters like `.`/`:` — e.g. an `escape:`-namespaced or dotted id).\n// Used by #extractToken to match the stream that re-renders THIS element by id.\nexport function escapeRegExp(string) {\n return string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n\nif (typeof window !== \"undefined\") {\n if (window.Turbo) registerReactiveActions()\n else document.addEventListener(\"turbo:load\", registerReactiveActions, { once: true })\n}\n\n// --- Registration guard (issue #26 part 2) -------------------------------\n// In a `lazyLoadControllersFrom(\"controllers\", application)` app, only\n// controllers under app/javascript/controllers/ are registered. This module\n// lives outside that dir, so importing it isn't enough — `data-controller=\n// \"reactive\"` does NOTHING until the host runs application.register(\"reactive\",\n// ...). The failure is silent: components render, but no action ever fires.\n//\n// We can't warn from connect() in that case (connect never runs). Instead, once\n// the page is ready, if reactive elements exist but no controller has connected,\n// the controller wasn't registered — so we warn, pointing at the fix.\nlet reactiveConnected = false\n\nexport function checkReactiveRegistration() {\n if (reactiveConnected) return\n if (typeof document === \"undefined\") return\n const els = document.querySelectorAll('[data-controller~=\"reactive\"]')\n if (!els || els.length === 0) return\n console.warn(\n \"[phlex-reactive] found \" + els.length + ' element(s) with data-controller=\"reactive\" ' +\n \"but the reactive controller never connected. It is loaded but not registered — \" +\n 'add `application.register(\"reactive\", ReactiveController)` (importmap) or import it ' +\n \"into app/javascript/controllers/ for lazyLoadControllersFrom apps. See the README.\"\n )\n}\n\n// Test seams (no-ops in production usage).\nexport function __resetReactiveRegistrationForTest() {\n reactiveConnected = false\n}\nexport function __markReactiveConnectedForTest() {\n reactiveConnected = true\n}\n\nif (typeof window !== \"undefined\" && typeof document !== \"undefined\") {\n // Defer past initial controller connection (a microtask/tick after ready).\n const scheduleCheck = () => setTimeout(checkReactiveRegistration, 0)\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", scheduleCheck, { once: true })\n } else {\n scheduleCheck()\n }\n}\n\n// The interpret-time attribute-name allowlist (issue #96) — the SECOND half of\n// the two-sided default-deny. The Ruby builder already refuses these at build\n// time; this guards a hand-built / forged ops attr from bypassing it. Refused:\n// event handlers (on*, XSS), URL-bearing names (a javascript: navigation\n// surface), and style (CSS injection). Case-insensitive, mirroring js.rb.\nconst REFUSED_ATTR_URL = new Set([\"href\", \"src\", \"srcdoc\", \"action\", \"formaction\", \"xlink:href\", \"style\"])\nfunction attrRefused(name) {\n const lower = String(name).toLowerCase()\n return lower.startsWith(\"on\") || REFUSED_ATTR_URL.has(lower)\n}\n\n// Run an animated visibility change (issue #96 `transition:`). `flip` performs\n// the actual hidden-flag change; `[during, from, to]` are class lists applied\n// AROUND it. Cleanup (removing during+to) is awaited via `animationend` OR a\n// setTimeout fallback — whichever comes first — so an element with NO animation\n// never leaves the helper classes stuck (the op chain itself is not blocked:\n// cleanup is fire-and-forget, later ops run immediately). The fallback and the\n// listener share a one-shot `done` guard so cleanup runs exactly once.\nfunction runTransition(el, transition, flip) {\n const [during, from, to] = transition\n el.classList.add(during, from)\n flip()\n requestAnimationFrame(() => {\n el.classList.remove(from)\n el.classList.add(to)\n })\n\n let done = false\n const cleanup = () => {\n if (done) return\n done = true\n el.classList.remove(during, to)\n }\n el.addEventListener(\"animationend\", cleanup, { once: true })\n // ~10% over a common 300ms transition; also the ONLY path for a non-animated\n // element (animationend never fires there), so it must always be scheduled.\n setTimeout(cleanup, 350)\n}\n\n// The client-op whitelist behind on_client (issue #95, extended in #96). Mirrors\n// Phlex::Reactive::JS's vocabulary; an op name not in this map is\n// warn-and-skipped by #applyOps (client-side default-deny — a stale or newer\n// ops attr must never break the page). Each op is a pure, local DOM mutation:\n// nothing is read back, nothing is sent anywhere. Frozen so nothing can be\n// registered into it at runtime — extending the vocabulary is a gem change,\n// not an app hook.\nconst CLIENT_OPS = Object.freeze({\n show: (el, args) => setHidden(el, false, args),\n hide: (el, args) => setHidden(el, true, args),\n toggle: (el, args) => setHidden(el, !el.hidden, args),\n add_class: (el, args) => el.classList.add(...(args.classes ?? [])),\n remove_class: (el, args) => el.classList.remove(...(args.classes ?? [])),\n toggle_class: (el, args) => (args.classes ?? []).forEach((c) => el.classList.toggle(c)),\n\n // Attribute ops (issue #96), interpret-time allowlisted. set_attr writes the\n // (already-stringified) value; toggle_attr adds a missing attr (value \"\") or\n // removes a present one; remove_attr removes it. A refused name warns + skips.\n set_attr: (el, args) => {\n if (guardAttr(args.name)) el.setAttribute(args.name, args.value ?? \"\")\n },\n remove_attr: (el, args) => {\n if (guardAttr(args.name)) el.removeAttribute(args.name)\n },\n toggle_attr: (el, args) => {\n if (!guardAttr(args.name)) return\n if (el.hasAttribute(args.name)) el.removeAttribute(args.name)\n else el.setAttribute(args.name, \"\")\n },\n\n // Focus ops (issue #96). focus targets the match itself; focus_first targets\n // its first focusable descendant (opened-menu → first menuitem).\n focus: (el) => el.focus?.(),\n focus_first: (el) => firstFocusable(el)?.focus?.(),\n\n // Text op (issue #159): set textContent — XSS-safe by construction (never\n // innerHTML), strictly less powerful than set_attr. Change-guarded like\n // #mirrorText. With global: true it is the cross-root text escape: paint a\n // value into a recap node OUTSIDE the component's root.\n text: (el, args) => {\n const text = String(args.value ?? \"\")\n if (el.textContent !== text) el.textContent = text\n },\n\n // Dispatch a bubbling CustomEvent (issue #96). RAW element.dispatchEvent — the\n // controller SHADOWS Stimulus's this.dispatch helper, so it must not be used.\n dispatch: (el, args) => {\n el.dispatchEvent(new CustomEvent(args.name, { bubbles: true, composed: true, detail: args.detail ?? {} }))\n },\n})\n\n// Apply a hidden-flag change, optionally animated by a [during, from, to]\n// transition (issue #96). Split out so show/hide/toggle share it.\nfunction setHidden(el, hidden, args) {\n if (args?.transition) runTransition(el, args.transition, () => (el.hidden = hidden))\n else el.hidden = hidden\n}\n\n// The interpret-time attribute guard: refuse (warn + skip) a name off the\n// allowlist. Returns true when the op may proceed.\nfunction guardAttr(name) {\n if (!attrRefused(name)) return true\n console.warn(`[phlex-reactive] refused client attr op on ${JSON.stringify(name)} — skipped`)\n return false\n}\n\n// A cross-root mirror target must be a single ID selector (issue #159) — \"#\" +\n// a CSS identifier, nothing else. The client half of the two-sided default-deny\n// (reactive_compute's `mirror:` validates the SAME shape loudly at declare\n// time): a hand-built mirror attr must not widen a declared text mirror into a\n// page-wide selector write. A refused selector warns + skips (its siblings\n// still apply), matching the attr-allowlist posture.\nconst MIRROR_ID_SELECTOR = /^#[A-Za-z_][\\w-]*$/\nfunction guardMirrorSelector(selector) {\n if (typeof selector === \"string\" && MIRROR_ID_SELECTOR.test(selector)) return true\n console.warn(`[phlex-reactive] refused cross-root mirror target ${JSON.stringify(selector)} — skipped`)\n return false\n}\n\n// Evaluate a show binding's declared literal predicate (issue #161) against\n// the controlling field's current value. Exactly one of the three predicate\n// attrs decides: equals (value === literal), not (value !== literal), in\n// (value ∈ a JSON string list). The vocabulary is fixed and literal-only —\n// never an expression, so there is no eval surface (the reactive_show helper\n// enforces the same shape loudly at render; this is the client half of the\n// two-sided posture). Returns true/false for a decidable binding, or null for\n// a malformed/missing predicate — the caller SKIPS a null so a hand-built or\n// stale binding never flips visibility it doesn't understand (default-deny,\n// like the op whitelist).\nfunction showBindingMatches(el, value) {\n const equals = el.getAttribute(\"data-reactive-show-equals\")\n if (equals !== null) return value === equals\n const not = el.getAttribute(\"data-reactive-show-not\")\n if (not !== null) return value !== not\n const inRaw = el.getAttribute(\"data-reactive-show-in\")\n if (inRaw !== null) {\n try {\n const list = JSON.parse(inRaw)\n if (Array.isArray(list)) return list.includes(value)\n } catch {\n // fall through to the warn below — malformed JSON and a non-array both skip\n }\n console.warn(`[phlex-reactive] malformed reactive_show in: list ${JSON.stringify(inRaw)} — skipped`)\n return null\n }\n // Numeric threshold predicates (issue #176 part B): gte/gt/lte/lt read the\n // literal off its own flat attr and compare Number(value) against it. Any\n // present numeric attr decides the binding — a non-numeric field value (NaN)\n // is false (hidden), and a non-numeric LITERAL warn-skips (null).\n for (const key of SHOW_NUMERIC_KEYS) {\n const raw = el.getAttribute(`data-reactive-show-${key}`)\n if (raw !== null) return numericPredicateMatches(key, raw, value)\n }\n console.warn(\"[phlex-reactive] a reactive_show binding declares no predicate — skipped\")\n return null\n}\n\n// The numeric threshold keys (issue #176 part B) — the client half of the Ruby\n// SHOW_NUMERIC_KEYS. Order-independent; the evaluator reads the one that's\n// present. Each coerces BOTH sides to Number and compares.\nconst SHOW_NUMERIC_KEYS = [\"gte\", \"gt\", \"lte\", \"lt\"]\n\n// Evaluate one numeric threshold predicate against a field value. Returns\n// true/false for a decidable comparison, or null when the LITERAL itself is\n// non-numeric (a malformed binding — warn-skip, default-deny). A non-numeric\n// FIELD value (empty/blank/garbage) is treated as NaN → false: the\n// reveal-on-threshold notice stays hidden, the safe default. Shared by the\n// owned-binding evaluator (raw string literal off an attr) and the\n// cross-root/compound evaluator (a literal that arrived as a JSON number or\n// string).\nfunction numericPredicateMatches(key, literal, value) {\n const rhs = Number(literal)\n if (Number.isNaN(rhs)) {\n console.warn(`[phlex-reactive] reactive_show ${key}: needs a numeric literal, got ${JSON.stringify(literal)} — skipped`)\n return null\n }\n // A blank/whitespace field value must fail closed. Number(\"\") and\n // Number(\" \") are 0 (NOT NaN), so a bare Number()+isNaN check would wrongly\n // reveal a `lte:`/`lt:`/`gte: 0` binding on an EMPTY field. Force the\n // empty/blank case to NaN so the \"blank → hidden\" contract holds for every\n // operator, not just the ones where 0 happens to fail the comparison.\n const trimmed = value == null ? \"\" : String(value).trim()\n const n = trimmed === \"\" ? NaN : Number(trimmed)\n if (Number.isNaN(n)) return false\n switch (key) {\n case \"gte\":\n return n >= rhs\n case \"gt\":\n return n > rhs\n case \"lte\":\n return n <= rhs\n case \"lt\":\n return n < rhs\n default:\n return null\n }\n}\n\n// Evaluate an ALREADY-PARSED show predicate object (issue #164) — the\n// reactive_show_targets map embeds { equals/not/in } directly in its JSON, so\n// unlike showBindingMatches there are no attrs to read or re-parse. The same\n// literal-only vocabulary; anything else (empty, unknown keys, a non-array\n// in:) returns null and the caller warn-skips that target (default-deny — a\n// hand-built map entry must never flip visibility it doesn't declare).\nfunction showPredicateMatches(pred, value) {\n if (!pred || typeof pred !== \"object\") return null\n if (typeof pred.equals === \"string\") return value === pred.equals\n if (typeof pred.not === \"string\") return value !== pred.not\n if (Array.isArray(pred.in)) return pred.in.includes(value)\n // Numeric threshold predicates (issue #176 part B): the literal arrives as a\n // JSON number (or a numeric string) embedded in the predicate object — one\n // shared numericPredicateMatches with the owned-binding evaluator.\n for (const key of SHOW_NUMERIC_KEYS) {\n if (key in pred) return numericPredicateMatches(key, pred[key], value)\n }\n return null\n}\n\n// The selector matching every OWNED-element show binding: single-field\n// (data-reactive-show-field, issue #161) OR compound all:/any:\n// (data-reactive-show, issue #176). Both the connect() gate and the sync walk\n// use it so a compound-only root still enables the sync.\nconst SHOW_BINDING_SELECTOR = \"[data-reactive-show-field], [data-reactive-show]\"\n\n// Parse a compound show binding's JSON payload (issue #176 part A). Malformed\n// JSON degrades to null WITH a warn — a bad binding must never throw or blank\n// the page (client-side default-deny), but a collision (two bindings' JSON\n// mix-joined) is worth surfacing.\nfunction parseShowCompound(raw) {\n try {\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed\n } catch {\n // fall through to the warn\n }\n console.warn(`[phlex-reactive] malformed compound reactive_show payload ${JSON.stringify(raw)} — skipped`)\n return null\n}\n\n// Evaluate one DNF TERM against a resolved field value (issue #180). A missing\n// field (null) or a malformed/unknown predicate folds to FALSE — fail-closed\n// (default-deny): a broken AND term can't pass, a broken OR term can't reveal.\nfunction dnfTermMatches(term, fieldValue) {\n if (!term || typeof term !== \"object\" || typeof term.field !== \"string\") return false\n // An absent owned field reads as \"\" — identical to the server evaluator\n // (ShowConditions.match? treats a missing field as blank). This keeps the\n // Ruby first-paint and the client live-toggle in exact agreement (the shared\n // fixture proves it). A malformed predicate still folds to false.\n const value = fieldValue(term.field) ?? \"\"\n return showPredicateMatches(term, value) === true\n}\n\n// Evaluate a DNF show payload (issue #180): { any: [group, …] } where each\n// GROUP is an array of terms (terms AND within a group, groups OR). Returns\n// true/false for a decidable payload, or null for a malformed one (no groups)\n// so the caller warn-skips and leaves visibility alone. This is the ONE shape\n// the 0.10 wire emits; showPayloadMatches routes the legacy shapes here or to\n// the compatibility arm below.\nfunction anyOfAllsMatches(groups, fieldValue) {\n if (!Array.isArray(groups) || groups.length === 0) return null\n // groups OR; within a group, terms AND (an empty group can't decide → false).\n return groups.some((group) => Array.isArray(group) && group.length > 0 &&\n group.every((term) => dnfTermMatches(term, fieldValue)))\n}\n\n// Route a parsed data-reactive-show payload to the right evaluator. The 0.10\n// wire is { any: [ [term,…], … ] } (DNF — groups are ARRAYS). For a stale tab\n// still serving pre-0.10 HTML (deploy overlap), fall back to the 0.9.5 compound\n// shape { all: [term,…] } / { any: [term,…] } where the values are flat TERM\n// OBJECTS, not arrays. The nesting distinguishes them: DNF's any[0] is an Array.\n// DELETE the legacy arm in 0.11.\nfunction showPayloadMatches(payload, fieldValue) {\n if (!payload || typeof payload !== \"object\") return null\n const any = payload.any\n if (Array.isArray(any) && (any.length === 0 || Array.isArray(any[0]))) {\n return anyOfAllsMatches(any, fieldValue)\n }\n return legacyCompoundShowMatches(payload, fieldValue)\n}\n\n// LEGACY (0.9.5, deploy-overlap only — DELETE in 0.11): the flat all:/any:\n// compound fold, where terms are objects (not groups). Preserved so a morph of\n// stale pre-0.10 HTML doesn't go dead.\nfunction legacyCompoundShowMatches(payload, fieldValue) {\n const connective = Array.isArray(payload.all) ? \"all\" : Array.isArray(payload.any) ? \"any\" : null\n if (!connective) return null\n const terms = payload[connective]\n if (terms.length === 0) return null\n const results = terms.map((term) => dnfTermMatches(term, fieldValue))\n return connective === \"all\" ? results.every(Boolean) : results.some(Boolean)\n}\n\n// A cross-root show target must be a single ID selector (issue #164) — the\n// SAME shape the #159 mirror enforces (one shared regex), with its own warn so\n// a refused show target is distinguishable in the console. The client half of\n// the two-sided default-deny: reactive_show_targets raises at declare time; a\n// hand-built wire attr must not widen the escape to class/compound selectors.\n// A refused selector warns + skips — its siblings still apply.\nfunction guardShowTargetSelector(selector) {\n if (typeof selector === \"string\" && MIRROR_ID_SELECTOR.test(selector)) return true\n console.warn(`[phlex-reactive] refused cross-root show target ${JSON.stringify(selector)} — skipped`)\n return false\n}\n\n// The first focusable descendant of `el`, in document order — the natural\n// keyboard target inside an opened menu/dialog. Covers the standard focusable\n// set; :not([tabindex=\"-1\"]) drops explicitly-removed nodes. Returns null when\n// nothing inside is focusable (focus_first then no-ops).\nconst FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\nfunction firstFocusable(el) {\n return el.querySelectorAll?.(FOCUSABLE)?.[0] ?? null\n}\n\n// Parse a [[name, args], ...] op list from a raw attr/param. An array passes\n// through; a JSON string is parsed; anything malformed degrades to [] — a bad\n// ops attr must NEVER break the page (client-side default-deny). Shared by the\n// controller's runOps and the reactive:js stream action (issue #97).\nfunction parseOps(raw) {\n if (Array.isArray(raw)) return raw\n if (typeof raw !== \"string\") return []\n try {\n const list = JSON.parse(raw)\n return Array.isArray(list) ? list : []\n } catch {\n return []\n }\n}\n\n// Interpret a [[name, args], ...] op list against the frozen CLIENT_OPS\n// whitelist (issues #95/#96/#97). `resolveTargets(args)` returns the element(s)\n// an op applies to — the controller scopes to its root (excluding nested\n// reactive roots); the reactive:js stream action scopes to its target root (or\n// the document). An unknown name warns and is SKIPPED while the rest of the\n// chain still applies — client-side default-deny, one bad op never takes down\n// its siblings. Object.hasOwn (not a bare read) so inherited Object members\n// (\"constructor\") can't masquerade as ops.\nfunction applyOps(list, resolveTargets) {\n for (const entry of list) {\n if (!Array.isArray(entry)) continue\n const [name, args = {}] = entry\n if (!Object.hasOwn(CLIENT_OPS, name)) {\n console.warn(`[phlex-reactive] unknown client op ${JSON.stringify(name)} — skipped`)\n continue\n }\n for (const el of resolveTargets(args)) CLIENT_OPS[name](el, args)\n }\n}\n\n// Resolve a reactive:js op's targets against its `target` root (issue #97).\n// \"@root\" is the root element itself; a selector resolves WITHIN it; a bare\n// selector with no root (no `target` attr on the stream) resolves document-wide\n// — a broadcast op anchored by a global selector (#bell) rather than a\n// component. Unlike the controller path there is no nested-reactive-root\n// ownership filter: a server-pushed op names its own scope explicitly —\n// including `global: true`, which opts a single op out of the target-root\n// scope to document-wide resolution (issue #159; the same escape the builder\n// documents for the controller path).\nfunction streamOpTargets(args, root) {\n const to = args.to\n if (root) {\n if (to === \"@root\") return [root]\n if (typeof to !== \"string\" || to === \"\") return []\n if (args.global) return [...document.querySelectorAll(to)]\n return [...root.querySelectorAll(to)]\n }\n // No target root: document-scoped. \"@root\" is meaningless here (nothing to\n // anchor to) → no-op; a selector matches document-wide.\n if (typeof to !== \"string\" || to === \"\" || to === \"@root\") return []\n return [...document.querySelectorAll(to)]\n}\n\n// Register this controller eagerly (not lazily) so a click immediately after\n// page load is never missed. The phlex-reactive engine auto-pins it with\n// preload: true for importmap apps; see the README for esbuild/webpack.\nexport default class extends Controller {\n static values = {\n token: String, // signed identity token (component + record gid/state)\n }\n\n #tokenCache // freshest token, threaded synchronously across queued requests\n #debounceTimers = new Map() // trigger element -> { timer, flush } pending dispatch\n #throttleTimers = new Map() // trigger element -> Map(action -> suppression timer)\n #actionPathCache // page-stable action path, resolved once per controller\n #timeoutMsCache // page-stable request timeout (ms), resolved once per controller (issue #101)\n #tokenRegexCache // { id, token, self } — #extractToken's two per-id RegExps, rebuilt on id change (issue #118)\n // Loading-state bookkeeping (issue #99). All keyed so overlapping enqueues\n // refcount correctly and never clobber each other:\n #busyPending = 0 // root aria-busy pending counter (remove only at zero)\n #busyActions = new Map() // action -> in-flight count (root's space-separated busy set + busy_on)\n #busyTokenCounts = new WeakMap() // element -> Map(action -> count): its data-reactive-busy token set\n #loadingSnapshots = new Map() // trigger element -> { count, disabled, text } refcounted snapshot\n // Dirty tracking (issue #103): the bound re-scan (turbo:morph-element) and the\n // navigate-away guard handlers, held so disconnect() can remove exactly them.\n #boundScanDirty\n #boundBeforeUnload\n #boundBeforeVisit\n // Show bindings (issue #161): the ONE delegated sync handler shared by the\n // root's input/change/turbo:morph-element listeners, held for teardown.\n #boundSyncShow\n // Option filtering (issue #163): the ONE delegated sync handler shared by the\n // root's input/turbo:morph-element listeners, held for teardown.\n #boundSyncFilter\n // Lazy initial mount (issue #165): the bound re-probe attached to\n // turbo:morph-element so a Turbo page-refresh morph re-fires the defer fetch.\n #boundProbeLazyDefer\n\n // Mark that a reactive controller actually connected, so the registration\n // guard above knows the controller was registered (issue #26 part 2).\n connect() {\n reactiveConnected = true\n\n // Root-id guard (issue #48). The token round trip assumes the reactive root\n // element's id == component.id: the server targets component.id and the client\n // self-matches its NEXT token by this.element.id (#extractToken, issue #46).\n // If `id:` landed on a CHILD instead of the `**reactive_attrs` root, this id is\n // \"\" — #extractToken falls back to the FIRST token in the response (a child's),\n // so the next action POSTs a foreign token → endpoint default-deny → silent 403.\n // Warn NOW (on connect) so the failure surfaces on page load, not on click 2.\n if (this.element.id === \"\") {\n console.warn(\n \"[phlex-reactive] a reactive root has no id; its next-action token can't self-match \" +\n \"and may fall back to the first token in the response → a silent HTTP 403 on the NEXT action. \" +\n \"Put id: on the SAME element as reactive_attrs — use div(**reactive_root) (emits id + token together), \" +\n \"or div(id:, **reactive_attrs). The id: must NOT be on a child. See the README.\"\n )\n }\n\n // Lazy initial mount (issue #165): a reactive_lazy shell carries its defer\n // token as a ROOT attribute — enter the SAME module-level fetch path a\n // reply directive uses (supersession, pending markers, error handling\n // included). Probe on connect (a plain replace / cache restoration\n // re-connects) AND on turbo:morph-element: a Turbo page-refresh MORPH\n // re-shows the shell while keeping the element CONNECTED and firing no\n // Stimulus lifecycle, so a connect-only probe would leave the morphed-in\n // shell shimmering forever. The supersession registry makes a duplicate\n // probe a no-op (same target id), so re-probing is safe. The attribute\n // stays on the shell precisely so a re-appearance re-fires.\n // Only wire the morph re-probe for a root that IS a lazy shell (carries the\n // token) — a component that never uses reactive_lazy pays nothing (no\n // listener), matching the dirty-tracking / show-sync gating precedent.\n if (this.element.getAttribute?.(\"data-reactive-defer-token\")) {\n this.#probeLazyDefer()\n this.#boundProbeLazyDefer = () => this.#probeLazyDefer()\n this.element.addEventListener?.(\"turbo:morph-element\", this.#boundProbeLazyDefer)\n }\n\n // Dirty tracking (issue #103) — ONLY when this root opts in (track_dirty: or a\n // reactive_field(dirty:)), so a component that never uses it pays nothing (no\n // baseline scan, no morph listener on every broadcast). A plain (outerHTML)\n // replace re-connects the controller, so seed the baseline scan here — the root\n // reflects current-vs-default WITHOUT waiting for the first input. An in-place\n // morph / broadcast morph keeps the element CONNECTED and fires no Stimulus\n // lifecycle, so ALSO listen for turbo:morph-element on this.element to re-scan\n // after the morph writes fresh default* attributes (reactive:applied is NOT a\n // valid hook — it fires when streams are handed to Turbo, BEFORE the DOM\n // mutation). Both listeners are torn down in disconnect().\n if (this.#dirtyTrackingEnabled()) {\n this.#boundScanDirty = () => this.#scanDirty()\n this.element.addEventListener?.(\"turbo:morph-element\", this.#boundScanDirty)\n this.#scanDirty()\n\n // warn_unsaved: arm a navigate-away guard gated on a LIVE dirty-count read\n // (never a cached snapshot — the count is re-derived from the DOM each time).\n // beforeunload covers a real browser unload; turbo:before-visit covers a\n // Turbo in-app navigation (it does NOT fire on restoration visits — the\n // documented gap). Registered on window only when the marker is present.\n if (this.element.getAttribute?.(\"data-reactive-warn-unsaved\") === \"true\") {\n this.#armUnsavedGuard()\n }\n }\n\n // Show bindings (issue #161) — ONLY when this root owns one, so a component\n // without any pays a single probe (the dirty-tracking gate precedent). ONE\n // delegated listener pair on the root (input + change bubble from every\n // owned field — no per-field wiring, and a reactive_compute output write\n // dispatches a real input event, so computed values drive visibility too).\n // The connect sync seeds the initial state — a plain replace re-connects —\n // and turbo:morph-element re-syncs after an in-place morph (which keeps the\n // element connected, fires no Stimulus lifecycle, and may preserve a\n // user-edited field value the server's hidden attrs don't reflect).\n if (this.#showSyncEnabled()) {\n this.#boundSyncShow = () => this.#syncShow()\n this.element.addEventListener?.(\"input\", this.#boundSyncShow)\n this.element.addEventListener?.(\"change\", this.#boundSyncShow)\n this.element.addEventListener?.(\"turbo:morph-element\", this.#boundSyncShow)\n this.#syncShow()\n }\n\n // Option filtering (issue #163) — ONLY when the root declares the binding\n // (reactive_filter emits both attrs together), so a component without one\n // pays two attribute reads. ONE delegated input listener on the root — the\n // handler re-filters only for events from the NAMED input, so keystrokes in\n // unrelated fields on a wide form never pay a filter pass. The connect sync\n // seeds from the input's current value (a plain replace re-connects; back\n // navigation may restore typed text), and turbo:morph-element re-applies\n // after an in-place morph (which keeps the element connected, fires no\n // Stimulus lifecycle, and may preserve the user's typed query while the\n // server re-rendered every option visible).\n if (this.#filterEnabled()) {\n this.#boundSyncFilter = (event) => {\n if (event?.type === \"input\" && !this.#filterInputEvent(event)) return\n this.#syncFilter()\n }\n this.element.addEventListener?.(\"input\", this.#boundSyncFilter)\n this.element.addEventListener?.(\"turbo:morph-element\", this.#boundSyncFilter)\n this.#syncFilter()\n }\n }\n\n // Whether this root opts into dirty tracking (issue #103): track_dirty: puts the\n // trackDirty descriptor on the ROOT's data-action; a per-field reactive_field(\n // dirty:) puts it on a descendant field. Either turns tracking on. A quick\n // attribute read + one scoped query, evaluated once per connect (a cold path).\n #dirtyTrackingEnabled() {\n if ((this.element.getAttribute?.(\"data-action\") ?? \"\").includes(\"reactive#trackDirty\")) return true\n const nodes = this.element.querySelectorAll?.('[data-action*=\"reactive#trackDirty\"]') ?? []\n for (const el of nodes) if (this.#ownsField(el)) return true\n return false\n }\n\n // Tear down any pending debounce timers when the controller leaves the DOM\n // (Turbo morph/navigation removes the element). Otherwise a timer that hasn't\n // fired yet would later call #enqueue on a disconnected controller — a round\n // trip against a detached element / stale token (issue #17 follow-up).\n // Throttle suppression timers (issue #80) are torn down the same way — a\n // leading-edge timer holds no pending POST, but leaving it running would leak\n // it past the element's life.\n disconnect() {\n this.#clearAllDebounces()\n this.#clearAllThrottles()\n this.#teardownDirtyTracking()\n this.#teardownShowSync()\n this.#teardownFilterSync()\n if (this.#boundProbeLazyDefer) {\n this.element.removeEventListener?.(\"turbo:morph-element\", this.#boundProbeLazyDefer)\n }\n }\n\n // Lazy initial mount probe (issue #165): fetch the real content when THIS\n // root is a reactive_lazy shell that still carries its defer token AND the\n // pending marker. Gating on the pending marker is what makes a re-probe (a\n // Turbo morph re-showing the shell) fire while a re-probe of an already\n // RESOLVED root (real content, no token, no marker) is a no-op. The\n // module-level supersession registry dedupes a duplicate in-flight fetch for\n // the same id, so calling this on both connect and every morph is safe.\n #probeLazyDefer() {\n const el = this.element\n if (!el?.id) return\n const token = el.getAttribute?.(\"data-reactive-defer-token\")\n if (!token) return\n if (el.getAttribute?.(\"data-reactive-defer-pending\") !== \"true\") return\n startFetchDefer(el.id, token)\n }\n\n // Serialize requests per component. Each round trip rewrites the signed\n // token in the DOM (state lives in the token, not the client). If events\n // fire faster than round trips complete, concurrent requests would all read\n // the SAME stale token and clobber each other (last-write-wins). Chaining on\n // a per-controller promise makes each dispatch wait for the previous one, so\n // it always uses the freshest token.\n dispatch(event) {\n // `window` (renamed: never shadow the global) and `outside` are the event-\n // modifier params (issue #80). The client decides preventDefault behavior\n // from event.params — set by the Ruby on() — never by sniffing the\n // Stimulus descriptor.\n const { action, params, debounce, throttle, confirm, outside, window: windowBound, optimistic, loading } =\n event.params\n if (!action) return\n\n // Outside guard FIRST (issue #80): an outside: trigger only fires for\n // events whose target is OUTSIDE this component's ROOT (containment against\n // this.element — .contains includes the root itself). An event inside the\n // root must be a COMPLETE no-op — before preventDefault (the page's native\n // click behavior is untouched) and before the reactive:before-dispatch\n // lifecycle event (nothing to announce, nothing to veto).\n if (outside && this.element.contains(event.target)) return\n\n // The trigger is event.currentTarget — the element on(...) was spread onto —\n // NOT event.target (issue #99). A `<button><span>Save</span></button>` click\n // has target === the span, which carries no params and is the wrong element\n // to disable / swap text on. currentTarget is the bound element; fall back to\n // target for a directly-invoked/synthetic event. Captured now because\n // #proceed runs in a later microtask (after the confirm resolver), by which\n // point currentTarget is reset to null.\n const target = event.currentTarget ?? event.target\n\n // Stop native behavior (button submit / FORM NAVIGATION) HERE, synchronously\n // within the event dispatch — BEFORE the (possibly async) confirm gate below.\n // preventDefault() only works while the event is still being handled; once we\n // await the confirm resolver it's too late, and a `submit` trigger would\n // natively POST the form and navigate before the reactive round trip runs\n // (issue #11). For a `click` trigger there's no default to miss. This holds\n // for debounced triggers too — the round trip is deferred, but the native\n // default must still be prevented now. (Moved ahead of the confirm branch in\n // issue #55: an async resolver means we can't preventDefault after awaiting.)\n //\n // ONLY for element-bound triggers: a window-bound trigger (window:/outside:,\n // issue #80) hears EVERY matching event on the page — preventDefault-ing\n // those would kill every link click while a dropdown is mounted. The page's\n // native behavior proceeds alongside the reactive round trip.\n //\n // The `checked: :keep` optimistic hint (issue #98) OPTS OUT: for a click-bound\n // checkbox/radio the unconditional preventDefault is exactly what stops the\n // native flip from happening before the morph — so a bare checkbox click\n // (which has no form-navigation default to lose) skips it and flips now, and\n // the failure revert snaps it back. A `change`-bound trigger is unaffected —\n // `change` isn't cancelable, so preventDefault was already a no-op there.\n if (!windowBound && !this.#keepsNativeToggle(optimistic, target)) event.preventDefault()\n\n // No confirm message → proceed straight away (unchanged fast path).\n if (!confirm) return this.#proceed(target, action, params, debounce, throttle, optimistic, loading)\n\n // Confirmation gate (issue #52, made overridable + async in #55). A reactive\n // trigger can't use Hotwire's data-turbo-confirm — this controller preempts\n // the event — so a `confirm:` message routes through confirmResolver (default\n // window.confirm; an app can override it to reuse Turbo.config.forms.confirm).\n // The resolver may be sync or async; call it INSIDE the chain (via the leading\n // .then) so even a SYNCHRONOUS override throw rejects this promise instead of\n // escaping dispatch — a throwing dialog is treated as a cancel, like the user\n // dismissing it. The .catch is scoped to the resolver step (→ false = cancel),\n // so a dismissed/erroring dialog never surfaces as an unhandled rejection AND a\n // genuine bug inside #proceed is NOT silently swallowed. Enqueue ONLY on a\n // truthy resolution — nothing is enqueued, no timer scheduled, otherwise.\n Promise.resolve()\n .then(() => confirmResolver(confirm))\n .catch(() => false)\n .then((ok) => {\n if (ok) this.#proceed(target, action, params, debounce, throttle, optimistic, loading)\n })\n }\n\n // CLIENT-ONLY trigger entry point (issue #95) — the zero-round-trip sibling\n // of dispatch(). Wired by on_client: applies the declared op chain\n // (data-reactive-ops-param, built by Phlex::Reactive::JS) locally. NO token,\n // NO params, NO fetch, ever. Ops are ephemeral UI: any server re-render of\n // the component resets whatever they toggled (by design — a signed action\n // owns state that must survive re-renders).\n runOps(event) {\n const { ops, outside, window: windowBound } = event.params\n\n // Outside guard FIRST — identical semantics to dispatch() (issue #80): an\n // outside: trigger is a COMPLETE no-op for events inside this root, before\n // preventDefault and before any op runs.\n if (outside && this.element.contains(event.target)) return\n\n // Element-bound triggers preventDefault (a bare button inside a <form>\n // must not submit it); window-bound triggers (window:/outside:) never do —\n // they hear every matching event on the page, and preventDefault-ing those\n // would kill native clicks site-wide (issue #80 rationale).\n if (!windowBound) event.preventDefault()\n\n this.#applyOps(this.#parseOps(ops))\n }\n\n // Dirty tracking (issue #103). Wired by reactive_field(dirty: true) /\n // reactive_root(track_dirty: true): an `input` on an owned field runs a FULL\n // re-scan of every field this root owns. NO round trip, NO shipped state — the\n // baseline is the DOM's own defaultValue/defaultChecked/defaultSelected (the\n // last server render). A full pass (not a per-target toggle) is essential for\n // radio groups: the deselected radio flips to checked=false and fires no input\n // event, so only re-scanning everything keeps its flag honest.\n trackDirty() {\n this.#scanDirty()\n }\n\n // Client-side compute (data binding). Wired by reactive_compute: an `input`\n // trigger (input->reactive#recompute) runs a REGISTERED JS reducer over the\n // named input fields and writes the named output fields WITH NO ROUND TRIP —\n // the \"instant\" half of the new/unpersisted-record UX. If the field ALSO\n // carries on(...) (a persisted record, or a synced draft), that debounced POST\n // still fires and the server reply reconciles; recompute just paints first.\n //\n // Reads inputs/outputs/reducer from the root's data-reactive-compute-* attrs\n // (set once by reactive_compute_attrs). A missing/unregistered reducer is a\n // no-op — a page must never break because a binding wasn't wired up.\n //\n // The reducer gets a second argument, meta = { changed } (issue #75): the\n // name of the declared input the triggering event edited, or null for a\n // direct call / an unowned or undeclared target. A multi-way rebalance\n // branches on it (edit c → derive a; else → derive c). Note that a #76\n // output write dispatches a real input event, so recompute RE-ENTERS with\n // changed = that output's name — the reducer must be convergent (see\n // compute.js) so the change guard settles the chain.\n recompute(event) {\n // Inputs may be a JSON ARRAY of names (array form — every input coerced\n // through Number, the shipped behavior) or a JSON OBJECT of name→type (hash\n // form, issue #104 — :number coerced, :string read raw). #parseComputeInputs\n // returns [name, type] pairs either way (array form defaults type \"number\").\n const inputPairs = this.#parseComputeInputs()\n const inputs = inputPairs.map(([name]) => name)\n\n // Resolve every declared input AND output through ONE per-call resolver whose\n // ownership probe is computed ONCE (issue #117), replacing the per-name\n // closest() walk #ownedField did on every read — a 30-field calculator paid\n // ~60 closest() sweeps per keystroke. #ownershipFilter returns a constant-true\n // predicate in the common no-nested-root case (skipping closest() entirely)\n // and the exact #ownsField check when a nested reactive root is present\n // (issue #15 scoping, byte-identical to before). Resolution is memoized in a\n // per-CALL Map, FIRST-WINS, so a name read as an input AND written as an\n // output resolves to the SAME element and is queried once.\n //\n // Why per-name `[name=\"X\"]` queries and not one bare `[name]` sweep: a single\n // sweep is the natural \"one walk\", but the resolver must issue the SAME\n // per-name query shape the field walk always has (the issue-#15 unit fakes\n // answer only `[name=\"X\"]`). It is O(distinct declared names) queries, not the\n // old O(inputs + outputs) — the ownership decision is hoisted out of the loop.\n // The memo is per-CALL only: an output write dispatches `input` (issue #76),\n // re-entering recompute, which correctly rebuilds a fresh map (a morph may\n // have replaced the nodes) — it is NEVER stored on the instance.\n const owns = this.#ownershipFilter()\n const byName = new Map()\n const ownedField = (name) => {\n if (byName.has(name)) return byName.get(name)\n let found = null\n for (const el of this.element.querySelectorAll(`[name=\"${name}\"]`)) {\n if (owns(el)) {\n found = el // FIRST-WINS (radio groups, Rails hidden+checkbox name pairs)\n break\n }\n }\n byName.set(name, found)\n return found\n }\n\n // Identity-mirror pass (issue #104), ALWAYS run — even with NO registered\n // reducer, so reactive_text(:title) mirrors a field into its text node with\n // zero reducer wiring. Each declared input's RAW string value is written to\n // its owned [data-reactive-text=\"<name>\"] node(s). It runs BEFORE the reducer\n // early-return below so a reducer-less binding still mirrors.\n for (const name of inputs) this.#mirrorText(name, ownedField(name)?.value ?? \"\")\n\n const key = this.element.getAttribute(\"data-reactive-compute-reducer-param\")\n const reduce = key ? computeReducer(key) : null\n if (!reduce) {\n // No reducer registered: the identity pass above still ran, so declared\n // cross-root mirrors of the INPUT names still paint (issue #159) — a\n // reducer-less binding mirrors, exactly like the owned-text-node case.\n this.#applyComputeMirrors({}, ownedField)\n return\n }\n\n const outputs = this.#parseComputeList(\"data-reactive-compute-outputs-param\")\n\n // Coerce each input per its declared type (issue #104): \"string\" → the raw\n // display string (blank/absent → \"\"); else (\"number\", the array-form default)\n // → the numeric coercion (blank/NaN → 0, the nanToZero the hand-written\n // calculators use). Reads from the memoized resolver — no re-query.\n const values = {}\n for (const [name, type] of inputPairs) {\n const field = ownedField(name)\n if (type === \"string\") {\n values[name] = field?.value ?? \"\"\n } else {\n const n = Number(field?.value)\n values[name] = Number.isFinite(n) ? n : 0\n }\n }\n\n // meta.changed stays on #changedComputeField (its own #ownsField check over\n // the raw event target) — NOT this resolver. The issue-#15 nested-rejection\n // test depends on that path being unchanged.\n const result = reduce(values, { changed: this.#changedComputeField(event, inputs) }) || {}\n for (const name of outputs) {\n if (!(name in result)) continue\n const field = ownedField(name)\n // Output resolution (issue #104): write to the owned named FIELD if one\n // exists, ELSE mirror to every owned [data-reactive-text=\"<name>\"] node.\n if (field) {\n // Real browsers do NOT fire `input` on a programmatic .value write (issue\n // #76), so after writing we dispatch a bubbling `input` ourselves — that's\n // what drives a chained repaint (a summary listener, a second compute),\n // matching the server's set_value + dispatch(\"input\") contract. The write\n // is CHANGE-GUARDED: an unchanged value is skipped entirely (no write, no\n // event). The guard is what lets a reducer with overlapping inputs/outputs\n // (the shipped payment_split shape) settle — an unconditional dispatch\n // would re-enter input->reactive#recompute forever.\n if (String(result[name]) === field.value) continue\n field.value = result[name]\n field.dispatchEvent(new Event(\"input\", { bubbles: true }))\n } else {\n // A text-node output: textContent, XSS-safe by construction. Change-\n // guarded too (compare before writing), but NO input dispatch — a text\n // node has no listener contract, so nothing chains off it.\n this.#mirrorText(name, result[name])\n }\n }\n\n // Cross-root text mirrors (issue #159) — AFTER the outputs are applied, so\n // a mirror keyed on a just-written output paints the settled value.\n this.#applyComputeMirrors(result, ownedField)\n }\n\n // Client-side list navigation (combobox keyboard nav, issue #72). Wired by\n // on(:search, …, listnav: \"[role=option]\"), which appends keyboard filters to\n // the input's data-action (keydown.down/up/enter/esc->reactive#listnav*) and\n // sets data-reactive-listnav-option-param. Arrow keys move a highlight among\n // the options WITH NO ROUND TRIP; Enter picks the highlighted option by\n // CLICKING IT (so its own on(:select) reactive trigger fires — selection stays\n // a signed action); Escape clears. Ephemeral highlight state lives on the DOM\n // (data-reactive-highlighted), never shipped to the client as trusted state.\n listnavNext(event) {\n this.#moveHighlight(event, +1)\n }\n\n listnavPrev(event) {\n this.#moveHighlight(event, -1)\n }\n\n // Enter: activate the highlighted option (fires its reactive select). No-op if\n // nothing is highlighted, and in that case DON'T preventDefault — Enter falls\n // through (there's no selection to make).\n listnavPick(event) {\n const options = this.#listnavOptions(event)\n const current = options.findIndex((el) => el.hasAttribute(\"data-reactive-highlighted\"))\n if (current < 0) return\n event.preventDefault()\n options[current].click()\n }\n\n listnavClose(event) {\n for (const el of this.#listnavOptions(event)) el.removeAttribute(\"data-reactive-highlighted\")\n }\n\n // Move the highlight by `step` (with wrap-around) among THIS root's options.\n // preventDefault stops Arrow keys from moving the caret in the search input.\n #moveHighlight(event, step) {\n const options = this.#listnavOptions(event)\n if (!options.length) return\n event.preventDefault()\n\n const current = options.findIndex((el) => el.hasAttribute(\"data-reactive-highlighted\"))\n // From nothing: Down highlights the first option, Up the last.\n const next = current < 0 ? (step > 0 ? 0 : options.length - 1) : (current + step + options.length) % options.length\n\n for (const el of options) el.removeAttribute(\"data-reactive-highlighted\")\n const chosen = options[next]\n chosen.setAttribute(\"data-reactive-highlighted\", \"true\")\n chosen.scrollIntoView?.({ block: \"nearest\" })\n }\n\n // The option elements this root owns (skips nested reactive roots, issue #15),\n // per the selector on data-reactive-listnav-option-param. The attr rides on the\n // TRIGGER element (the search input on(...) is spread onto), read from the\n // event; the options are still scoped to this controller's root. Empty when\n // unset. Falls back to the root for a directly-invoked call (unit tests). The\n // ownership predicate is hoisted ONCE per keypress (issue #117) — in the common\n // no-nested-root case it is a constant true, skipping a closest() walk per\n // option. Hidden options are excluded (issue #163): a reactive_filter (or any\n // `hidden` toggle) removes a row from the keyboard path too, so an Arrow can't\n // highlight — and Enter can't pick — an invisible option.\n #listnavOptions(event) {\n const trigger = event?.currentTarget ?? event?.target ?? this.element\n const selector =\n trigger.getAttribute?.(\"data-reactive-listnav-option-param\") ??\n this.element.getAttribute(\"data-reactive-listnav-option-param\")\n if (!selector) return []\n const owns = this.#ownershipFilter()\n return Array.from(this.element.querySelectorAll(selector)).filter((el) => !el.hidden && owns(el))\n }\n\n // Parse a JSON string list from a root data attr; [] on absence/parse error so\n // a malformed binding degrades to \"no fields\" rather than throwing on input.\n #parseComputeList(attr) {\n const raw = this.element.getAttribute(attr)\n if (!raw) return []\n try {\n const list = JSON.parse(raw)\n return Array.isArray(list) ? list : []\n } catch {\n return []\n }\n }\n\n // Parse the inputs param into [name, type] pairs (issue #104). The wire is a\n // JSON ARRAY of names (array form → every input typed \"number\", the shipped\n // numeric coercion) OR a JSON OBJECT of name→type (hash form → \":string\" read\n // raw, \":number\" coerced). Malformed/absent degrades to [] — a bad binding\n // must never throw on input.\n #parseComputeInputs() {\n const raw = this.element.getAttribute(\"data-reactive-compute-inputs-param\")\n if (!raw) return []\n try {\n const parsed = JSON.parse(raw)\n if (Array.isArray(parsed)) return parsed.map((name) => [name, \"number\"])\n if (parsed && typeof parsed === \"object\") return Object.entries(parsed)\n return []\n } catch {\n return []\n }\n }\n\n // The declared compute input the event just edited — the reducer's\n // meta.changed (issue #75). The triggering field counts only when it is a\n // named form control OWNED by this root (not a nested reactive root's, issue\n // #15) AND its name is among the declared compute inputs; anything else\n // (a direct call, an unowned/undeclared target) yields null.\n #changedComputeField(event, inputs) {\n const target = event?.target\n if (!target?.name || typeof target.closest !== \"function\") return null\n if (!inputs.includes(target.name)) return null\n return this.#ownsField(target) ? target.name : null\n }\n\n // Write `value` into every owned [data-reactive-text=\"<name>\"] node via\n // textContent (issue #104) — XSS-safe by construction (never innerHTML). Drives\n // both the identity mirror (an input's raw value) and a text-node output (a\n // reducer result with no matching field). Change-guarded (skip an unchanged\n // node) and NO input dispatch — a text node has no listener contract. String()\n // so a numeric result renders like the DOM would.\n #mirrorText(name, value) {\n const text = String(value)\n for (const node of this.#ownedTextNodes(name)) {\n if (node.textContent === text) continue\n node.textContent = text\n }\n }\n\n // Every [data-reactive-text=\"<name>\"] mirror OWNED by this root (skips nested\n // reactive roots, issue #15). Empty when none — reactive_text is optional.\n #ownedTextNodes(name) {\n const nodes = this.element.querySelectorAll(`[data-reactive-text=\"${name}\"]`)\n return Array.from(nodes).filter((el) => this.#ownsField(el))\n }\n\n // Cross-root text mirrors (issue #159): paint every DECLARED mirror name into\n // its allowlisted document-wide id targets via textContent — the opt-in escape\n // from root isolation (issue #15) for a recap OUTSIDE the computing root. The\n // value is the reducer's result when it produced one, else the owned field's\n // CURRENT value (an input identity mirror / a just-written output) — one\n // declaration covers all three shapes. A name with NO value this pass is\n // SKIPPED (a mirror never blanks a recap the reducer didn't feed). textContent\n // only (never innerHTML), change-guarded, and NO input dispatch — same\n // contract as #mirrorText. With no mirror declared this is one getAttribute\n // and out — the shipped compute path never touches the document.\n #applyComputeMirrors(result, ownedField) {\n const mirror = this.#parseComputeMirror()\n for (const [name, selectors] of Object.entries(mirror)) {\n const value = name in result ? result[name] : ownedField(name)?.value\n if (value === undefined || value === null) continue\n const text = String(value)\n for (const sel of Array.isArray(selectors) ? selectors : [selectors]) {\n if (!guardMirrorSelector(sel)) continue\n for (const node of document.querySelectorAll(sel)) {\n if (node.textContent === text) continue\n node.textContent = text\n }\n }\n }\n }\n\n // The declared cross-root mirror map (issue #159): a JSON object of\n // { name: [id selectors] } from data-reactive-compute-mirror-param (emitted by\n // reactive_compute's `mirror:`). Absent/malformed degrades to {} — a bad\n // binding must never throw on input.\n #parseComputeMirror() {\n const raw = this.element.getAttribute(\"data-reactive-compute-mirror-param\")\n if (!raw) return {}\n try {\n const parsed = JSON.parse(raw)\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? parsed : {}\n } catch {\n return {}\n }\n }\n\n // Enqueue the action — debounced if a debounce window is set, else immediately.\n // Split out of dispatch so both the no-confirm fast path and the post-confirm\n // microtask share one place (issue #55). `target` is captured up front because\n // this can run in a later microtask, after event.target has been reset.\n #proceed(target, action, params, debounce, throttle, optimistic, loading) {\n // Lifecycle veto point (issue #79): one cancelable reactive:before-dispatch\n // per user gesture — post-preventDefault, post-confirm, PRE-debounce (and\n // PRE-throttle, the same timing). event.preventDefault() skips the\n // debounce/throttle AND the enqueue entirely (nothing is scheduled).\n // retry() re-enters the queue directly, so this does NOT refire on a retry.\n // detail.params are the trigger's explicit params; sibling fields are\n // collected later, at send time.\n const before = this.#emit(\"reactive:before-dispatch\", {\n action,\n params: this.#parseParams(params),\n element: this.element,\n }, { cancelable: true })\n if (before.defaultPrevented) return\n\n // Debounced trigger (e.g. on(:update, event: \"input\", debounce: 300)):\n // coalesce rapid events into ONE round trip after a quiet period, instead of\n // one POST per keystroke (issue #17). A blur flushes a pending dispatch.\n // The optimistic hint (issue #98) and the loading state (issue #99) ride to\n // the flush too, so they apply ONCE per enqueue — a debounced input must not\n // flap toggle_class per keystroke, and its element must NOT be disabled\n // during the quiet period (that would break typing). Both apply at ENQUEUE.\n const ms = Number(debounce) || 0\n if (ms > 0) return this.#debounceDispatch(target, ms, action, params, optimistic, loading)\n\n // Throttled trigger (e.g. on(:track, event: \"scroll\", window: true,\n // throttle: 250), issue #80): LEADING-EDGE rate limit — fire the first\n // event immediately, drop the rest until the window elapses. debounce and\n // throttle are mutually exclusive (the Ruby on() raises on both).\n const throttleMs = Number(throttle) || 0\n if (throttleMs > 0) return this.#throttleDispatch(target, throttleMs, action, params, optimistic, loading)\n\n return this.#enqueue(action, params, optimistic, target, loading)\n }\n\n // Apply the optimistic hint ONCE (recording its inverse) and chain the round\n // trip, threading that inverse onto THIS queued request so the serialized\n // per-controller queue reverts the RIGHT request's hint on failure (issue\n // #98). Applying here — the single flush/enqueue point every path funnels\n // through — is what makes a hint apply once per enqueue, not per raw dispatch.\n //\n // The loading state (issue #99) applies here too, for the same reason: enqueue\n // is the moment the request is committed to the queue, so the always-on busy\n // vocabulary (data-reactive-busy on the trigger + root, aria-busy via a pending\n // counter, busy_on scoping) and the loading hint (disable + class + text swap)\n // cover the WHOLE pending window — queue wait included — not just the fetch. It\n // returns a `settle` closure that #perform runs in its finally (success OR\n // failure), guarded so a morph-replaced trigger is never clobbered.\n #enqueue(action, params, optimistic, target, loading) {\n const inverse = this.#applyOptimistic(optimistic, target)\n const settle = this.#applyLoading(action, target, loading)\n this.queue = (this.queue ?? Promise.resolve()).then(() => this.#perform(action, params, inverse, settle))\n return this.queue\n }\n\n // Reset a per-element timer; only enqueue the round trip after `ms` of quiet.\n // Also flush immediately on blur so leaving the field never drops the last\n // edit (a long debounce shouldn't swallow a value the user tabbed away from).\n #debounceDispatch(target, ms, action, params, optimistic, loading) {\n this.#clearDebounce(target)\n\n const flush = () => {\n this.#clearDebounce(target)\n this.#enqueue(action, params, optimistic, target, loading)\n }\n const timer = setTimeout(flush, ms)\n target?.addEventListener?.(\"blur\", flush, { once: true })\n this.#debounceTimers.set(target, { timer, flush })\n }\n\n #clearDebounce(target) {\n const pending = this.#debounceTimers.get(target)\n if (!pending) return\n clearTimeout(pending.timer)\n target?.removeEventListener?.(\"blur\", pending.flush)\n this.#debounceTimers.delete(target)\n }\n\n // Clear every pending debounce timer (used on disconnect). Reuses\n // #clearDebounce so all timer/listener teardown stays in one place. Snapshot\n // the keys first — #clearDebounce mutates the map as it goes.\n #clearAllDebounces() {\n for (const target of [...this.#debounceTimers.keys()]) this.#clearDebounce(target)\n }\n\n // Leading-edge throttle (issue #80), mirroring #debounceDispatch: the FIRST\n // event fires immediately; a suppression timer then drops further events\n // until the window elapses (no trailing fire — dropped, not queued). Timers\n // are keyed on action + target, NOT target alone: window-bound scroll/resize\n // events all share event.target === document, so two window-bound triggers\n // on one component would otherwise collide on one timer.\n #throttleDispatch(target, ms, action, params, optimistic, loading) {\n const timers = this.#throttleTimers.get(target) ?? new Map()\n if (timers.has(action)) return // inside the window — suppress\n\n const timer = setTimeout(() => {\n timers.delete(action)\n if (timers.size === 0) this.#throttleTimers.delete(target)\n }, ms)\n timers.set(action, timer)\n this.#throttleTimers.set(target, timers)\n return this.#enqueue(action, params, optimistic, target, loading) // leading edge: fire NOW\n }\n\n // Clear every throttle suppression timer (used on disconnect, alongside\n // #clearAllDebounces) so nothing outlives the element.\n #clearAllThrottles() {\n for (const timers of this.#throttleTimers.values()) {\n for (const timer of timers.values()) clearTimeout(timer)\n }\n this.#throttleTimers.clear()\n }\n\n // Raw-dispatch a lifecycle CustomEvent (issue #79). Deliberately NOT\n // Stimulus's this.dispatch() helper — that name is SHADOWED by this\n // controller's own dispatch(event) action method. Bubbling + composed so a\n // page-level listener (or `data-action=\"reactive:error->toast#show\"` on an\n // ancestor) hears it. After a plain (non-morph) replace this.element is a\n // DETACHED node — a bubbling event on it never reaches document listeners —\n // so fall back to dispatching on document itself.\n #emit(name, detail, { cancelable = false } = {}) {\n const event = new CustomEvent(name, { bubbles: true, composed: true, cancelable, detail })\n const root = this.element.isConnected ? this.element : document\n root.dispatchEvent(event)\n return event\n }\n\n // reactive:error detail: { action, params, kind, status?, body?, retry }.\n // `params` are the FULL params that were sent (collected fields + explicit\n // trigger params). retry() re-enters the request queue with the ORIGINAL raw\n // trigger params, so #perform re-reads the freshest token and RE-COLLECTS the\n // sibling fields — nothing stale is replayed. It does not refire\n // reactive:before-dispatch (one veto per user gesture), and it no-ops with a\n // warning once the root has left the DOM (retrying against a detached\n // element would post a stale token into nowhere).\n #emitError(action, rawParams, sentParams, extra) {\n const retry = () => {\n if (!this.element.isConnected) {\n console.warn(\"[phlex-reactive] retry() ignored — the reactive root left the DOM\")\n return\n }\n return this.#enqueue(action, rawParams)\n }\n this.#emit(\"reactive:error\", { action, params: sentParams, ...extra, retry })\n }\n\n // Mark the reactive root as errored (issue #100) with the failure kind, so an\n // app can style it purely in CSS ([data-reactive-error] { … }) with zero JS.\n // Guarded — a plain replace may have detached the root before the failure lands.\n #markError(kind) {\n if (this.element?.isConnected === false) return\n this.element?.setAttribute?.(\"data-reactive-error\", kind)\n }\n\n // Clear the failure marker on the next successful apply (issue #100).\n #clearError() {\n this.element?.removeAttribute?.(\"data-reactive-error\")\n }\n\n // Offline fallback (issue #100): a network failure reached no server, so there\n // is no body to render. Clone the content of a server-rendered\n // <template data-reactive-error-flash> into the flash region so the user still\n // sees SOMETHING. The template is app-authored (trusted) — this is a pure\n // deep clone, never client templating of untrusted data. No template, or no\n // flash container, is a silent no-op (a page without the opt-in is unchanged).\n #renderNetworkFallback() {\n const template = document.querySelector(\"[data-reactive-error-flash]\")\n if (!template?.content) return\n // The flash region is the host-app container Response#flash targets. Its id\n // defaults to \"flash\" (Phlex::Reactive.flash_target); an app that customized\n // it can point the fallback at the same node by putting the template's\n // data-reactive-error-flash value there — but the common case is #flash.\n const targetId = template.getAttribute(\"data-reactive-error-flash\") || \"flash\"\n const region = document.getElementById(targetId)\n if (!region) return\n region.appendChild(template.content.cloneNode(true))\n }\n\n // Latency simulator (issue #102): if enableLatencySim(ms) stored a delay in\n // sessionStorage, await it before the fetch so the busy window (already open\n // since enqueue) is actually visible on localhost. Reads the key LIVE per\n // request — like the CSRF token — so toggling the sim mid-session takes effect\n // on the very next action without a reload. A missing sessionStorage, an absent\n // key, or a non-positive/NaN value resolves immediately (no timer, no delay) —\n // the whole feature is inert for any app that never opts in. Warns ONCE while\n // active (module-level guard), not once per request.\n #maybeSimulateLatency() {\n if (typeof sessionStorage === \"undefined\") return Promise.resolve()\n const ms = Number(sessionStorage.getItem(LATENCY_KEY))\n if (!Number.isFinite(ms) || ms <= 0) return Promise.resolve()\n if (!latencyBannerShown) {\n latencyBannerShown = true\n console.warn(\n `[phlex-reactive] latency simulator ACTIVE — every action is delayed by ${ms}ms. ` +\n \"Call PhlexReactive.disableLatencySim() (or clear sessionStorage) to turn it off.\",\n )\n }\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n\n // Client debug mode (issue #108) — the \"devtools-lite\" lens. On when the Ruby\n // reactive_attrs stamped data-reactive-debug=\"true\" (Phlex::Reactive.debug).\n // Read live (a single getAttribute) so the whole feature is inert for any app\n // that never opts in: OFF → this returns false and #perform builds no debug\n // object, parses no response, and logs nothing (the \"zero cost when off\"\n // invariant — one nil-check per dispatch). Guarded for a stub root with no\n // getAttribute (unit harnesses) so it degrades to off, never throwing.\n #debugEnabled() {\n return this.element?.getAttribute?.(\"data-reactive-debug\") === \"true\"\n }\n\n // A monotonic timestamp for the round-trip duration (ms). performance.now is\n // monotonic (immune to a wall-clock adjustment mid-request); Date.now is the\n // fallback for an exotic environment without it. ONLY called on the debug path,\n // so it costs nothing when debug is off.\n #debugNow() {\n return typeof performance !== \"undefined\" && typeof performance.now === \"function\"\n ? performance.now()\n : Date.now()\n }\n\n // Parse a turbo-stream response's action + target pairs for the debug trace,\n // from the body text #perform ALREADY read (never a re-fetch). NAMES only — the\n // <template> contents (rendered HTML, the fresh token) are deliberately not\n // touched. A non-turbo-stream / empty body yields [] (nothing to report).\n #debugStreams(body) {\n if (!body) return []\n const streams = []\n const re = /<turbo-stream\\b([^>]*)>/g\n let match\n while ((match = re.exec(body)) !== null) {\n const attrs = match[1]\n const action = attrs.match(/\\baction=\"([^\"]*)\"/)?.[1] ?? \"?\"\n const target = attrs.match(/\\btarget=\"([^\"]*)\"/)?.[1]\n streams.push(target ? `${action} → #${target}` : action)\n }\n return streams\n }\n\n // console.group ONE dispatch (issue #108). Carries NAMES + outcomes ONLY — the\n // signed token VALUE and every field/param VALUE are deliberately absent (they\n // may be sensitive; the whole point is observability without leaking data). The\n // caller passes the info it already holds so nothing is recomputed or re-fetched:\n // { action, paramNames, fieldNames, encoding, status, streams, tokenRefreshed, ms }\n // `console.groupCollapsed` keeps the console tidy (one collapsed line per action).\n #logDispatch(info) {\n const { action, status, ms } = info\n // The client can't name the component CLASS (it's inside the signed, opaque\n // token — never decoded here), but the root's id is the stable client-side\n // handle (e.g. #todo_42), so the header reads `reactive #todo_42 rename → …`.\n const who = this.element?.id ? `#${this.element.id} ` : \"\"\n const header = `reactive ${who}${action} → ${status ?? \"—\"} (${Math.round(ms)}ms)`\n /* eslint-disable no-console */\n console.groupCollapsed(header)\n console.log(`params: [${info.paramNames.join(\", \")}] + collected: [${info.fieldNames.join(\", \")}]`)\n console.log(`encoding: ${info.encoding}`)\n if (info.streams.length) console.log(`streams: ${info.streams.join(\" \")}`)\n console.log(`token: ${info.tokenRefreshed ? \"refreshed ✓\" : \"unchanged\"}`)\n console.groupEnd()\n /* eslint-enable no-console */\n }\n\n async #perform(action, params, inverse, settle) {\n // Auto-collect named field values inside this component so a button-\n // triggered action still receives sibling inputs (Livewire-style), plus any\n // chosen file inputs in the SAME walk. Explicit params\n // (data-reactive-params-param) win over collected fields.\n const { fields, files } = this.#collectFields()\n // Parse the explicit trigger params ONCE — reused below for allParams and, on\n // the debug path, for the params-only name list (so the trace can show\n // `params: [...]` distinct from the collected `+ collected: [...]`). #parseParams\n // is pure (a fresh object from a JSON string, or the same object by reference\n // when already parsed); allParams spreads a COPY and nothing mutates parsedParams\n // downstream (JSON.stringify / #buildFormData read allParams, a new object).\n const parsedParams = this.#parseParams(params)\n const allParams = { ...fields, ...parsedParams }\n const token = this.#currentToken\n\n // File/multipart path (issue #34): if THIS root has a populated\n // <input type=\"file\">, the action can't be JSON (JSON.stringify drops the\n // File). Send FormData instead — token + act + scalar params as fields, each\n // chosen file appended. The morph/token machinery downstream is identical;\n // only the request ENCODING differs when files are present.\n const multipart = files.length > 0\n const body = multipart\n ? this.#buildFormData(token, action, allParams, files)\n : JSON.stringify({ token, act: action, params: allParams })\n\n // Client debug mode (issue #108): build the trace object ONLY when debug is on\n // (one nil-check otherwise — zero cost off). It carries NAMES only: the\n // explicit trigger param names and the collected sibling field names, split so\n // the group shows `params: [...] + collected: [...]`. status/streams/\n // tokenRefreshed are filled in at the branch that knows them; the group is\n // emitted once in the finally so EVERY exit (success or any failure) logs.\n const debug = this.#debugEnabled()\n ? {\n action,\n paramNames: Object.keys(parsedParams),\n fieldNames: Object.keys(fields),\n encoding: multipart ? \"multipart\" : \"json\",\n status: null,\n streams: [],\n tokenRefreshed: false,\n started: this.#debugNow(),\n }\n : null\n\n // aria-busy on the root is now driven by the loading pending counter\n // (#applyLoading, applied at ENQUEUE so it covers the queue wait too), not\n // set here. #settleLoading in the finally clears it — see #enqueue.\n\n // Latency simulator (issue #102): the busy window (aria-busy + loading state)\n // is already applied at enqueue, so awaiting the configured delay HERE — after\n // that window opened and before the fetch — is what makes it visible on\n // localhost. A null/absent/non-positive sessionStorage key is a no-op (zero\n // production surface), so this line vanishes for any app that never opts in.\n await this.#maybeSimulateLatency()\n\n try {\n // Offline gate (issue #101), authoritative at the NETWORK BOUNDARY (send\n // time), not at enqueue. A click can enqueue while online and reach here\n // after going offline (a debounced/queued request, a rapid transition) —\n // gating in #perform makes the kind consistently \"offline\" for that whole\n // condition instead of leaking through as a \"network\" fetch throw. The\n // fetch never fires, so the edit is not half-sent; the finally still runs\n // (settle clears loading), the optimistic hint reverts, and retry() (which\n // re-enters #perform) re-checks and sends once back online.\n if (navigator.onLine === false) {\n this.#revertOptimistic(inverse)\n this.#markError(\"offline\")\n this.#emitError(action, params, allParams, { kind: \"offline\" })\n return\n }\n\n let response\n try {\n const headers = {\n Accept: \"text/vnd.turbo-stream.html\",\n \"X-CSRF-Token\": this.#csrfToken(),\n }\n // For JSON we declare the content type; for multipart we must NOT — the\n // browser sets `multipart/form-data; boundary=…` itself, and overriding it\n // would strip the boundary and corrupt the body server-side.\n if (!multipart) headers[\"Content-Type\"] = \"application/json\"\n // Send the pgbus SSE connection id (if subscribed) so the server can\n // exclude this connection from its own broadcast echo — the actor\n // already gets the action's HTTP response. Harmless without pgbus.\n const connectionId = this.#connectionId()\n if (connectionId) headers[\"X-Pgbus-Connection\"] = connectionId\n\n // ONLY `fetch` itself is the network boundary — offline, DNS, a reset\n // connection. Everything below this inner try (reading the body,\n // extracting the token, handing streams to Turbo) runs AFTER the\n // server already processed the mutation, so none of it belongs in the\n // `kind: \"network\"` / retriable bucket (CodeRabbit review on #89): if\n // renderStreamMessage throws, retry()ing would re-POST an action the\n // server already completed — see the outer catch below. (NOT a\n // reactive:applied LISTENER throwing — per the DOM spec, dispatchEvent\n // never propagates a listener's exception back to its caller, so that\n // case can't reach this catch at all; verified in the JS test suite.)\n response = await fetch(this.#actionPath(), {\n method: \"POST\",\n headers,\n body,\n credentials: \"same-origin\",\n // Bound the request (issue #101): a server that never answers used to\n // wedge this.queue forever (the finally that clears aria-busy/loading\n // never ran). AbortSignal.timeout(ms) aborts the fetch after the\n // configured window; the abort surfaces in this catch as a\n // DOMException named \"TimeoutError\" (see the branch below).\n signal: AbortSignal.timeout(this.#timeoutMs()),\n })\n } catch (error) {\n console.error(\"[phlex-reactive] action error\", error)\n this.#revertOptimistic(inverse)\n // AbortSignal.timeout() rejects with a DOMException named \"TimeoutError\"\n // (a manual AbortController.abort() would be \"AbortError\" — we don't use\n // one, but accept it too for robustness). A timeout is NOT \"offline\":\n // the request left and the server didn't answer in time; connectivity is\n // unknown. So do NOT clone the offline-fallback template or mark network\n // — just fire kind:\"timeout\" (retriable). The queue still advances\n // (#perform returns, never rejects), so the hung request un-wedges it.\n if (error?.name === \"TimeoutError\" || error?.name === \"AbortError\") {\n this.#markError(\"timeout\")\n this.#emitError(action, params, allParams, { kind: \"timeout\" })\n return\n }\n // No server reached — nothing to render (issue #100). Clone a\n // server-rendered <template data-reactive-error-flash> into the flash\n // region as an offline fallback. The template is rendered by the app\n // (trusted), so this is a pure clone — no client templating of data.\n this.#renderNetworkFallback()\n this.#markError(\"network\")\n this.#emitError(action, params, allParams, { kind: \"network\" })\n return\n }\n\n // Debug (issue #108): the server answered — record the status for the trace\n // now, so every response branch below (redirected/http/content-type/ok) logs\n // it. The transport-failure branches above return before here (they have no\n // status); their group still fires from the finally with status null → \"—\".\n if (debug) debug.status = response.status\n\n if (response.redirected) {\n console.error(\"[phlex-reactive] action was redirected (auth/CSRF?) — no update applied\")\n this.#revertOptimistic(inverse)\n this.#markError(\"redirected\")\n this.#emitError(action, params, allParams, { kind: \"redirected\", status: response.status })\n return\n }\n if (!response.ok) {\n const errorBody = await response.text()\n console.error(`[phlex-reactive] action failed: HTTP ${response.status}`, errorBody)\n this.#revertOptimistic(inverse)\n // Render a non-OK turbo-stream body so a server-rendered error flash\n // (an error_flash rescue, or a status: :unprocessable_entity validation\n // reply from a plain controller) is actually SHOWN — instead of being\n // read only for the console (issue #100). #extractToken is run as usual;\n // it NO-OPS when no stream re-renders our id, so a 400 InvalidToken body\n // never refreshes the held identity token (which is not a nonce and stays\n // retry-valid — do not \"fix\" that). A non-turbo-stream body is left to the\n // console.error above (an HTML error page must not be handed to Turbo).\n if ((response.headers.get(\"Content-Type\") || \"\").includes(\"turbo-stream\")) {\n const fresh = this.#extractToken(errorBody)\n this.#currentToken = fresh ?? this.#currentToken\n if (debug) this.#debugRecordBody(debug, errorBody, fresh)\n window.Turbo.renderStreamMessage(errorBody)\n }\n this.#markError(\"http\")\n this.#emitError(action, params, allParams, { kind: \"http\", status: response.status, body: errorBody })\n return\n }\n\n const contentType = response.headers.get(\"Content-Type\") || \"\"\n if (!contentType.includes(\"turbo-stream\")) {\n console.error(`[phlex-reactive] expected a turbo-stream, got \"${contentType}\" — no update applied`)\n this.#revertOptimistic(inverse)\n this.#markError(\"content-type\")\n this.#emitError(action, params, allParams, { kind: \"content-type\", status: response.status })\n return\n }\n\n const html = await response.text()\n // Capture the new token from the response synchronously, so the next\n // queued request uses it without waiting for the async DOM morph.\n const fresh = this.#extractToken(html)\n this.#currentToken = fresh ?? this.#currentToken\n // Debug (issue #108): record the stream actions/targets + whether a refresh\n // arrived, from the body we JUST read (reuse — no second text() read). Never\n // the token or template contents.\n if (debug) this.#debugRecordBody(debug, html, fresh)\n // Turbo applies the <turbo-stream> ops by id. A plain replace is an\n // outerHTML swap (focus on the replaced subtree is lost); a method=\"morph\"\n // replace (Response.morph) or an update morphs in place, preserving the\n // focused input + caret on unchanged nodes — see issue #28.\n window.Turbo.renderStreamMessage(html)\n // A successful apply CLEARS any prior failure marker (issue #100), so\n // error-driven CSS on the root (a red border, a shake) resets on recovery.\n this.#clearError()\n // Lifecycle hook (issue #79): the streams were HANDED TO Turbo — a\n // renderStreamMessage applies asynchronously, so the DOM mutation may\n // complete a tick later. Apps needing post-morph timing listen to Turbo's\n // own events; this one is for \"the action round trip succeeded\".\n this.#emit(\"reactive:applied\", { action, params: allParams, html })\n } catch (error) {\n // The server already processed this action successfully (we're past the\n // fetch) — a throw here is a CLIENT-side apply failure (a malformed\n // response, a broken Turbo render — NOT a reactive:applied listener\n // throw, which dispatchEvent never propagates here), not a transport\n // failure. kind: \"apply\" carries NO retry() — retrying would re-POST an\n // action the server already completed.\n console.error(\"[phlex-reactive] action error\", error)\n this.#revertOptimistic(inverse)\n this.#emit(\"reactive:error\", { action, params: allParams, kind: \"apply\" })\n } finally {\n // Settle the loading state (issue #99): decrement the pending counter,\n // drop the trigger's/root's busy tokens, restore disabled/text/class —\n // guarded so a morph-replaced trigger is never clobbered. Runs on EVERY\n // exit (success, every failure branch, or an apply throw).\n settle?.()\n // Debug (issue #108): emit the group here so EVERY exit path logs exactly\n // once — success, any transport/response failure, or an apply throw. Null\n // when debug is off (zero cost). The round-trip ms is measured now, at the\n // finally, so it spans the whole #perform (fetch + apply) regardless of exit.\n if (debug) this.#logDispatch({ ...debug, ms: this.#debugNow() - debug.started })\n }\n }\n\n // Debug (issue #108): fold the response body #perform already read into the\n // trace — the stream action/target pairs and whether a token refresh arrived\n // (a boolean; the token VALUE is intentionally not stored). Shared by the\n // success and the non-OK-turbo-stream branches so both log the same shape.\n #debugRecordBody(debug, body, freshToken) {\n debug.streams = this.#debugStreams(body)\n debug.tokenRefreshed = freshToken != null\n }\n\n get #currentToken() {\n return this.#tokenCache ?? this.tokenValue\n }\n\n set #currentToken(value) {\n this.#tokenCache = value\n }\n\n // Read the next token for THIS controller — the one that re-renders THIS\n // element's id, never just the first token in the body (issue #46). On a\n // collection of REACTIVE rows the prepended/appended ROW carries its OWN\n // data-reactive-token-value and it sorts FIRST in the response; the list's own\n // fresh token rides a trailing `reactive:token` stream targeting the container.\n // Grabbing the first match stored the ROW's token, so the list's SECOND\n // dispatch sent a row token → failed verification → add-once-only. This mirrors\n // the server's carries_token_for? (#44): a stream carries OUR token only when it\n // RE-RENDERS our id (reactive:token / replace / update of `this.element.id`) —\n // append/prepend insert children and never count. Returns undefined when no\n // stream re-renders our id, so #currentToken keeps its existing value.\n #extractToken(html) {\n const id = this.element.id\n if (!id) {\n // No id to self-match (shouldn't happen for a reactive root). Fall back to\n // the legacy first-token behavior so a single-component response still works.\n return html.match(/data-reactive-token-value=\"([^\"]+)\"/)?.[1]\n }\n\n const { token, self } = this.#tokenRegexes(id)\n\n // The dedicated token-only refresh for THIS element (partial updates / the\n // collection container) — an attribute on the <turbo-stream> itself.\n const tokenStream = html.match(token)\n if (tokenStream) return tokenStream[1]\n\n // A full self re-render: a replace/update of THIS element whose template root\n // carries the fresh token. Scope the token search to that one stream so a\n // sibling/child token elsewhere in the body can't leak in.\n const selfStream = html.match(self)\n if (selfStream) return selfStream[1].match(/data-reactive-token-value=\"([^\"]+)\"/)?.[1]\n\n // Nothing re-rendered our id — keep the current token.\n return undefined\n }\n\n // The two per-id RegExps #extractToken uses to self-match this element's next\n // token (issue #118). `this.element.id` is page-stable, so these are compiled\n // ONCE and reused across every response — instead of allocating two fresh\n // RegExps per round trip. The memo is KEYED ON THE ID and rebuilt when it\n // changes: a re-render that re-identifies the root must scan for the NEW target,\n // never the stale one (or the token would freeze — see the id-change bun test).\n // The PATTERNS are byte-identical to the pre-memo inline literals; only their\n // allocation moved.\n #tokenRegexes(id) {\n const cache = this.#tokenRegexCache\n if (cache && cache.id === id) return cache\n const escaped = escapeRegExp(id)\n return (this.#tokenRegexCache = {\n id,\n token: new RegExp(\n `<turbo-stream\\\\b[^>]*\\\\baction=\"reactive:token\"[^>]*\\\\btarget=\"${escaped}\"[^>]*\\\\bdata-reactive-token-value=\"([^\"]+)\"`,\n ),\n self: new RegExp(\n `<turbo-stream\\\\b[^>]*\\\\baction=\"(?:replace|update)\"[^>]*\\\\btarget=\"${escaped}\"[^>]*>([\\\\s\\\\S]*?)</turbo-stream>`,\n ),\n })\n }\n\n // True when `el` is collected by THIS reactive root and not by a nested one.\n // A reactive component can be rendered inside another (both are\n // data-controller=\"reactive\" roots). querySelectorAll() descends into nested\n // roots, so without this guard an outer action would sweep the inner roots'\n // inputs into its own params (issue #15). An element belongs to this root iff\n // its nearest [data-controller~=\"reactive\"] ancestor is this.element.\n #ownsField(el) {\n return el.closest('[data-controller~=\"reactive\"]') === this.element\n }\n\n // A per-op ownership PREDICATE — the issue #117 fast path over issue #15\n // scoping. #ownsField answers \"is this element mine, not a nested reactive\n // root's\" with a per-element closest() walk; on a wide form or every keystroke\n // that walk runs per matched field. This hoists the DECISION to once per op.\n //\n // HYBRID GATE — closest() stays the source of truth; the nested-root query only\n // decides whether the fast path is SAFE:\n // * Fast path (overwhelmingly common): this root contains NO nested reactive\n // roots, so every element the caller's querySelectorAll returned is already\n // a direct descendant of this.element with no intervening reactive root —\n // it is ours. Return a constant-true predicate and skip the per-field\n // closest() walk entirely. That is the whole win.\n // * Nested case: fall back to the UNCHANGED #ownsField closest() check, so\n // scoping is byte-identical to before. We deliberately do NOT use\n // contains() here — the closest() form needs no node to implement\n // contains(), and on a real DOM the two agree for a descendant of\n // this.element (el.closest('[data-controller~=\"reactive\"]') === this.element\n // iff no nested reactive-root descendant contains el).\n //\n // Computed ONCE per dispatch-scoped op (per #collectFields call, per recompute,\n // per #listnavOptions) and NEVER stored on the instance — a morph replaces\n // nodes, so a cached predicate would close over stale roots.\n #ownershipFilter() {\n const nested = this.element.querySelectorAll('[data-controller~=\"reactive\"]')\n if (nested.length === 0) return () => true\n return (el) => this.#ownsField(el)\n }\n\n // One walk over THIS root's named controls (not a nested reactive root's),\n // returning both the scalar `fields` and any chosen `files`. The ownership\n // predicate is hoisted ONCE (issue #117) via #ownershipFilter — in the common\n // no-nested-root case it is a constant true, so we skip a closest() walk per\n // field. A file input's `.value` is the useless \"C:\\fakepath\\…\" string, never a\n // scalar — so its chosen files are collected separately (honoring `multiple`)\n // and it adds no phantom blank value (issue #34). An empty `files` keeps the\n // JSON path.\n #collectFields() {\n const fields = {}\n const files = []\n const owns = this.#ownershipFilter() // compute ONCE per dispatch (issue #117)\n this.element.querySelectorAll(\"input[name], select[name], textarea[name]\").forEach((field) => {\n if (!owns(field)) return\n if (field.type === \"file\") {\n // Carry the input's `multiple` flag so #buildFormData keeps the array\n // shape (params[name][]) even when the user picked exactly one file —\n // otherwise a [:file] schema would see a lone scalar upload and drop it.\n for (const file of field.files ?? []) files.push({ name: field.name, file, multiple: field.multiple })\n } else if (field.type === \"checkbox\") {\n fields[field.name] = field.checked\n } else if (field.type === \"radio\") {\n if (field.checked) fields[field.name] = field.value\n } else {\n fields[field.name] = field.value\n }\n })\n // Named rich-text / custom editors (lexxy-editor, trix-editor) and bare\n // [contenteditable]. These aren't input/select/textarea, so the query above\n // skips them — without this, a reactive save posts an empty value and\n // silently wipes the field (issue #8). Read whatever the element exposes:\n // a custom editor's serialized `.value`, else its contenteditable text.\n // Only fill a name the standard controls left absent or empty, so a synced\n // hidden input (e.g. Trix mirrors into one) still wins when populated.\n this.element\n .querySelectorAll(\"[name]:is(lexxy-editor, trix-editor, [contenteditable=''], [contenteditable=true], [contenteditable=plaintext-only])\")\n .forEach((el) => {\n if (!owns(el)) return // reuse the SAME hoisted predicate (nested reactive root — issue #15)\n // A plain element (e.g. a <div contenteditable>) has no `name` IDL\n // property — only the attribute — so read getAttribute, not el.name.\n const name = el.getAttribute(\"name\")\n if (!name) return\n const existing = fields[name]\n if (existing == null || existing === \"\") {\n fields[name] = el.value ?? el.textContent ?? el.innerHTML ?? \"\"\n }\n })\n return { fields, files }\n }\n\n // Re-compute the dirty flag for EVERY field this root owns in one pass (issue\n // #103), then reflect the total onto the root. Called on an owned field's input\n // (trackDirty), on connect (baseline seed), and after a turbo:morph-element\n // re-render (fresh default* attrs). dirty = current ≠ the DOM's own default:\n // checkbox/radio → checked !== defaultChecked\n // select → some option.selected !== option.defaultSelected\n // else → value !== defaultValue\n // A full pass (not per-target) is REQUIRED: a radio group's previously-checked\n // radio flips to checked=false with NO input event, so per-target toggling\n // would leave its flag stale. File inputs are skipped — a file has no server\n // default baseline. Per-dirty-field data-reactive-dirty=\"true\" (\"true\" STRING,\n // not a valueless boolean attr — mirrors the on() flag convention); the root\n // carries data-reactive-dirty=\"<count>\" and DROPS the attr at zero, so\n // `[data-reactive-dirty]` styles the whole form and `[data-reactive-dirty]`\n // on a field styles just the changed control — both pure CSS, zero JS.\n #scanDirty() {\n // Runs at bootstrap (the connect baseline seed) as well as on input/morph, so\n // it must never throw — degrade to a no-op if the root can't be queried (a real\n // reactive root always can; this guards minimal/test element stubs).\n if (typeof this.element?.querySelectorAll !== \"function\") return\n\n let count = 0\n this.element.querySelectorAll(\"input[name], select[name], textarea[name]\").forEach((field) => {\n if (!this.#ownsField(field)) return // skip a nested reactive root's fields (issue #15)\n if (field.type === \"file\") return // no server default baseline to diff against\n\n if (this.#fieldDirty(field)) {\n field.setAttribute(\"data-reactive-dirty\", \"true\")\n count++\n } else {\n field.removeAttribute(\"data-reactive-dirty\")\n }\n })\n\n if (count > 0) this.element.setAttribute(\"data-reactive-dirty\", String(count))\n else this.element.removeAttribute(\"data-reactive-dirty\")\n }\n\n // Whether a single owned control differs from its server-rendered default.\n #fieldDirty(field) {\n if (field.type === \"checkbox\" || field.type === \"radio\") {\n return field.checked !== field.defaultChecked\n }\n if (field.tag === \"select\" || field.options) {\n // Any option whose selected state diverges from its defaultSelected. Guard\n // for a stub/absent options list (degrade to clean).\n return Array.from(field.options ?? []).some((o) => o.selected !== o.defaultSelected)\n }\n return field.value !== field.defaultValue\n }\n\n // The live dirty-field count, re-derived from the DOM (never a cached snapshot)\n // — the source of truth for the warn_unsaved guard's gate.\n #dirtyCount() {\n const raw = this.element.getAttribute?.(\"data-reactive-dirty\")\n const n = Number(raw)\n return Number.isFinite(n) && n > 0 ? n : 0\n }\n\n // Arm the navigate-away guard (warn_unsaved: true, issue #103). beforeunload\n // blocks a real browser unload; turbo:before-visit blocks a Turbo in-app\n // navigation (it does NOT fire on restoration visits — the documented gap).\n // Both read the LIVE dirty count, so a clean form never blocks. Handlers are\n // stored so disconnect() removes exactly them.\n #armUnsavedGuard() {\n if (typeof window === \"undefined\" || typeof window.addEventListener !== \"function\") return\n\n this.#boundBeforeUnload = (event) => {\n if (this.#dirtyCount() === 0) return undefined\n // The spec dance: preventDefault + a truthy returnValue triggers the native\n // \"leave site?\" prompt. The string is legacy (modern browsers show their own\n // copy) but must be non-empty/truthy to arm the dialog.\n event.preventDefault()\n event.returnValue = \"You have unsaved changes.\"\n return event.returnValue\n }\n this.#boundBeforeVisit = (event) => {\n if (this.#dirtyCount() === 0) return\n const ok = typeof window.confirm === \"function\" ? window.confirm(\"You have unsaved changes. Leave anyway?\") : true\n if (!ok) event.preventDefault?.()\n }\n\n window.addEventListener(\"beforeunload\", this.#boundBeforeUnload)\n window.addEventListener(\"turbo:before-visit\", this.#boundBeforeVisit)\n }\n\n // Remove the dirty-tracking listeners on disconnect (Turbo morph/navigation) so\n // a morph re-scan or a navigate-away guard never runs against a detached root.\n #teardownDirtyTracking() {\n if (this.#boundScanDirty) {\n this.element.removeEventListener?.(\"turbo:morph-element\", this.#boundScanDirty)\n this.#boundScanDirty = undefined\n }\n if (typeof window !== \"undefined\" && typeof window.removeEventListener === \"function\") {\n if (this.#boundBeforeUnload) window.removeEventListener(\"beforeunload\", this.#boundBeforeUnload)\n if (this.#boundBeforeVisit) window.removeEventListener(\"turbo:before-visit\", this.#boundBeforeVisit)\n }\n this.#boundBeforeUnload = undefined\n this.#boundBeforeVisit = undefined\n }\n\n // Whether this root owns a show binding (issue #161) or declares cross-root\n // show targets (issue #164) — the connect() gate, so a component with\n // neither pays only this probe (the #dirtyTrackingEnabled precedent). A\n // NESTED root's bindings don't count: its own controller instance syncs them\n // (issue #15 ownership). The targets attr is checked FIRST — one\n // getAttribute, cheaper than the binding walk.\n #showSyncEnabled() {\n if (this.element.getAttribute?.(\"data-reactive-show-targets\")) return true\n // Single-field bindings carry -field; compound all:/any: bindings (issue\n // #176) carry data-reactive-show and have NO single controlling field, so\n // both selectors gate the sync.\n const nodes = this.element.querySelectorAll?.(SHOW_BINDING_SELECTOR) ?? []\n for (const el of nodes) if (this.#ownsField(el)) return true\n return false\n }\n\n // Re-evaluate every OWNED show binding in one pass (issue #161): read the\n // controlling field's current value, evaluate the declared literal predicate,\n // toggle `hidden`. A full pass (not per-target) for the same reason as\n // #scanDirty — a radio group's deselected radio fires no event — and because\n // several bindings can hang off one field (the value read is memoized per\n // pass). A binding whose field can't be resolved, or whose predicate is\n // malformed, leaves visibility ALONE — a bad binding must never break or\n // blank the page (client-side default-deny).\n #syncShow() {\n if (typeof this.element?.querySelectorAll !== \"function\") return\n\n const owns = this.#ownershipFilter()\n const scope = this.element.getAttribute?.(\"data-reactive-scope\") || null\n const values = new Map()\n // A memoized resolver shared by every binding in this pass — a field driving\n // several bindings (and several DNF terms) reads exactly once. Scope-aware:\n // a bare field `director` resolves as `[name=\"scope[director]\"]` (issue #180).\n const fieldValue = (name) => {\n if (!values.has(name)) values.set(name, this.#showFieldValue(name, owns, scope))\n return values.get(name)\n }\n for (const el of this.element.querySelectorAll(SHOW_BINDING_SELECTOR)) {\n if (!owns(el)) continue // a nested root's binding is its own controller's job\n\n // The 0.10 DNF payload (issue #180): data-reactive-show carries\n // { any: [ [term,…], … ] }. The legacy flat-attr and 0.9.5-compound read\n // arms live in showPayloadMatches/showBindingMatches for deploy overlap.\n const payloadRaw = el.getAttribute(\"data-reactive-show\")\n if (payloadRaw !== null) {\n const match = showPayloadMatches(parseShowCompound(payloadRaw), fieldValue)\n if (match !== null) this.#applyShowVisibility(el, match, owns, scope)\n continue\n }\n\n // LEGACY flat-attr binding (pre-0.10, deploy overlap — removed in 0.11).\n const name = el.getAttribute(\"data-reactive-show-field\")\n if (!name) continue\n const value = fieldValue(name)\n if (value === null) continue // no owned field with that name — leave it be\n const match = showBindingMatches(el, value)\n if (match === null) continue // malformed predicate — warned + skipped\n this.#applyShowVisibility(el, match, owns, scope)\n }\n\n // The cross-root pass (issue #164) shares the same owned-field memo, so a\n // field driving both an owned binding and an outside target reads once.\n this.#syncShowTargets(owns, values, scope)\n }\n\n // Toggle `hidden` (and, when the binding declares data-reactive-show-disable,\n // the `disabled` of every owned named control inside it) from a match result\n // (issue #180). Disabling a hidden section's controls stops them submitting —\n // the stale-value fix. A visible section re-enables them. Controls a nested\n // reactive root owns are left alone (#15 ownership).\n #applyShowVisibility(el, match, owns, scope) {\n el.hidden = !match\n if (el.getAttribute(\"data-reactive-show-disable\") !== \"true\") return\n if (typeof el.querySelectorAll !== \"function\") return\n for (const control of el.querySelectorAll(\"input[name], select[name], textarea[name]\")) {\n if (owns(control)) control.disabled = !match\n }\n // The element itself may be a named control (a bare field with a binding).\n if (el.name && owns(el)) el.disabled = !match\n }\n\n // Apply the declared cross-root show targets (issue #164) — the visibility\n // parallel of #applyComputeMirrors. For each declared field: read the OWNED\n // field's current value (never a nested root's — you can only drive outside\n // visibility from a field this root owns), then for each \"#id\" → predicate\n // entry: guard the selector id-only (warn-and-skip; the Ruby helper raised\n // at declare time — two-sided default-deny), resolve it DOCUMENT-WIDE, and\n // toggle `hidden`. A target id not on the page is silently skipped (an\n // unrendered tab pane is normal); a malformed predicate warn-skips its one\n // target while siblings still apply. With no map declared this is one\n // getAttribute and out.\n #syncShowTargets(owns, values, scope) {\n const map = this.#parseShowTargets()\n for (const [name, targets] of Object.entries(map)) {\n if (!targets || typeof targets !== \"object\" || Array.isArray(targets)) continue\n if (!values.has(name)) values.set(name, this.#showFieldValue(name, owns, scope))\n const value = values.get(name)\n if (value === null) continue // no owned field with that name — leave them be\n // Every target's terms share this one field, so a constant resolver folds\n // the group (issue #180): a target's value is a DNF GROUP (terms ANDed).\n const resolve = () => value\n for (const [selector, group] of Object.entries(targets)) {\n if (!guardShowTargetSelector(selector)) continue\n // 0.10 wire: the value is a DNF GROUP (an array of terms, ANDed).\n // LEGACY (0.9.5, deploy overlap — DELETE in 0.11): a flat predicate\n // OBJECT ({ equals/not/in/gte… }) routed through showPredicateMatches.\n let match\n if (Array.isArray(group)) {\n if (group.length === 0) {\n console.warn(`[phlex-reactive] malformed reactive_show_targets group for ${selector} — skipped`)\n continue\n }\n match = group.every((term) => dnfTermMatches(term, resolve))\n } else {\n const legacy = showPredicateMatches(group, value)\n if (legacy === null) {\n console.warn(`[phlex-reactive] malformed reactive_show_targets predicate for ${selector} — skipped`)\n continue\n }\n match = legacy\n }\n for (const node of document.querySelectorAll(selector)) node.hidden = !match\n }\n }\n }\n\n // The declared cross-root show-target map (issue #164): a JSON object of\n // { field: { \"#id\": predicate } } from data-reactive-show-targets (emitted\n // by reactive_show_targets on the root). Absent degrades to {}; malformed\n // degrades to {} WITH a warn — never a throw (the #parseComputeMirror\n // contract), but never silent either: the likeliest cause is TWO\n // reactive_show_targets calls on one root, whose JSON strings Phlex `mix`\n // space-joined into an unparseable attr. The warn names the fix.\n #parseShowTargets() {\n const raw = this.element.getAttribute?.(\"data-reactive-show-targets\")\n if (!raw) return {}\n try {\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed\n } catch {\n // fall through to the shared warn below\n }\n console.warn(\n \"[phlex-reactive] malformed data-reactive-show-targets — ignored. \" +\n \"Did two reactive_show_targets calls collide on one root? Declare every field in ONE call: \" +\n \"reactive_show_targets(mode: { ... }, kind: { ... })\"\n )\n return {}\n }\n\n // The current value of the OWNED field controlling a show binding, as the\n // string the literal predicate compares against. Mirrors #collectFields'\n // per-kind reads: a checkbox reports its checked state (\"true\"/\"false\" — its\n // .value is the constant \"on\", and the checkbox wins over the hidden input\n // Rails pairs with it); a radio group reports the CHECKED radio's value (\"\"\n // when none is); anything else reports .value first-wins. Returns null when\n // no owned field carries the name — the caller then leaves visibility alone.\n #showFieldValue(name, owns, scope) {\n // Scope (issue #180): a bare field `director` under `data-reactive-scope=\n // \"form\"` resolves as `[name=\"form[director]\"]`. A name already carrying a\n // bracket (a raw wire name the author passed) is used verbatim.\n const domName = scope && !name.includes(\"[\") ? `${scope}[${name}]` : name\n let sawRadio = false\n let first = null\n for (const el of this.element.querySelectorAll(`[name=\"${domName}\"]`)) {\n if (!owns(el)) continue\n if (el.type === \"checkbox\") return el.checked ? \"true\" : \"false\"\n if (el.type === \"radio\") {\n if (el.checked) return el.value ?? \"\"\n sawRadio = true\n continue\n }\n first ??= el\n }\n if (first) return first.value ?? \"\"\n return sawRadio ? \"\" : null\n }\n\n // Remove the show-sync listeners on disconnect, so a stray event after a\n // Turbo morph/navigation never re-evaluates against a detached root.\n #teardownShowSync() {\n if (!this.#boundSyncShow) return\n this.element.removeEventListener?.(\"input\", this.#boundSyncShow)\n this.element.removeEventListener?.(\"change\", this.#boundSyncShow)\n this.element.removeEventListener?.(\"turbo:morph-element\", this.#boundSyncShow)\n this.#boundSyncShow = undefined\n }\n\n // Whether this root declares an option filter (issue #163) — the connect()\n // gate. reactive_filter always emits input + option together, so requiring\n // BOTH also default-denies a half-built hand-authored binding.\n #filterEnabled() {\n return !!(\n this.element.getAttribute?.(\"data-reactive-filter-input\") &&\n this.element.getAttribute?.(\"data-reactive-filter-option\")\n )\n }\n\n // Whether a delegated input event came from the NAMED filter input (issue\n // #163). Anything else — another field's keystroke, a target without\n // matches() — skips the filter pass (the morph re-sync path bypasses this).\n #filterInputEvent(event) {\n const selector = this.element.getAttribute(\"data-reactive-filter-input\")\n return !!selector && typeof event.target?.matches === \"function\" && event.target.matches(selector)\n }\n\n // Re-apply the filter in one pass (issue #163): lowercase the named input's\n // current value, toggle `hidden` on every OWNED option by a substring match\n // against its haystack (data-reactive-filter-text, falling back to the\n // option's own text), collapse any group whose every contained option is\n // hidden, and reveal the empty target at 0 visible. A filtered-out option\n // also loses its listnav highlight so Enter can never pick an invisible row.\n // No owned input → leave visibility ALONE — a binding that can't resolve\n // must never break or blank the page (client-side default-deny). All\n // selectors resolve within this root, skipping nested reactive roots'\n // elements (issue #15 ownership; the predicate is hoisted once per pass).\n #syncFilter() {\n if (typeof this.element?.querySelectorAll !== \"function\") return\n const inputSelector = this.element.getAttribute(\"data-reactive-filter-input\")\n const optionSelector = this.element.getAttribute(\"data-reactive-filter-option\")\n if (!inputSelector || !optionSelector) return\n\n const owns = this.#ownershipFilter()\n const input = [...this.element.querySelectorAll(inputSelector)].find(owns)\n if (!input) return\n\n const query = (input.value ?? \"\").trim().toLowerCase()\n let visible = 0\n for (const el of this.element.querySelectorAll(optionSelector)) {\n if (!owns(el)) continue // a nested root's option is its own controller's job\n const haystack = (el.getAttribute(\"data-reactive-filter-text\") ?? el.textContent ?? \"\").toLowerCase()\n const hidden = query !== \"\" && !haystack.includes(query)\n el.hidden = hidden\n if (hidden) el.removeAttribute(\"data-reactive-highlighted\")\n else visible++\n }\n\n const groupSelector = this.element.getAttribute(\"data-reactive-filter-group\")\n if (groupSelector) {\n for (const group of this.element.querySelectorAll(groupSelector)) {\n if (!owns(group)) continue\n const contained = [...group.querySelectorAll(optionSelector)].filter(owns)\n // A group with no options isn't this filter's to decide — server state\n // stands (it may be a header the app toggles by other means).\n if (contained.length === 0) continue\n group.hidden = contained.every((el) => el.hidden)\n }\n }\n\n const emptySelector = this.element.getAttribute(\"data-reactive-filter-empty\")\n if (emptySelector) {\n for (const el of this.element.querySelectorAll(emptySelector)) {\n if (owns(el)) el.hidden = visible > 0\n }\n }\n }\n\n // Remove the filter-sync listeners on disconnect, so a stray event after a\n // Turbo morph/navigation never re-filters against a detached root.\n #teardownFilterSync() {\n if (!this.#boundSyncFilter) return\n this.element.removeEventListener?.(\"input\", this.#boundSyncFilter)\n this.element.removeEventListener?.(\"turbo:morph-element\", this.#boundSyncFilter)\n this.#boundSyncFilter = undefined\n }\n\n // Build the multipart body (issue #34). `token`/`act` are flat fields the\n // endpoint reads from params[:token]/params[:act]; scalar params nest under\n // params[<key>] (Rails parses the bracket into params[:params]); each file is\n // appended under params[<name>] (single) — a second file with the same name\n // (a `multiple` picker, several inputs sharing a name) is sent as\n // params[<name>][] so Rails coerces it to an array for a [:file] schema.\n #buildFormData(token, action, params, files) {\n const fd = new FormData()\n fd.append(\"token\", token)\n fd.append(\"act\", action)\n for (const [key, value] of Object.entries(params)) {\n this.#appendField(fd, `params[${key}]`, value)\n }\n const multiNames = this.#multiFileNames(files)\n for (const { name, file, multiple } of files) {\n // params[name][] when the input is `multiple` (array shape even for one\n // file) OR the name repeats across inputs; otherwise a lone scalar file.\n const asArray = multiple || multiNames.has(name)\n const key = asArray ? `params[${name}][]` : `params[${name}]`\n fd.append(key, file, file.name)\n }\n return fd\n }\n\n // Append a param leaf to FormData under its bracketed key. FormData carries\n // only strings, so a NON-scalar param (a nested object or an array) is\n // bracket-EXPANDED into params[key][sub] / params[key][index][...] fields —\n // the SAME Rails-form shape the server's expand_bracket_keys / array_values\n // already parse, so a JSON body and a multipart body coerce identically\n // (issue #39). Previously a non-scalar was JSON.stringify'd into one\n // params[key]='<json>' field, which the server received as an un-decodable\n // String leaf and DROPPED (nested hash -> {}, array -> key removed).\n //\n // Arrays use NUMERIC indices (params[key][0], params[key][1]) — required for\n // an array-of-hash so each element's sub-keys stay grouped and the server's\n // index-hash sort rebuilds order; params[key][] would collapse them. A scalar\n // (string/number/boolean) is one string field, mirroring the JSON wire shape\n // (the server's :boolean/:integer casts read \"true\"/\"42\"). null/undefined is\n // an empty field. An EMPTY array/object emits NOTHING — FormData can't carry\n // []/{}, so the key is omitted and the action's keyword default applies (this\n // differs from the JSON path, where an explicit [] coerces to an empty array).\n #appendField(fd, key, value) {\n if (value == null) {\n fd.append(key, \"\")\n } else if (Array.isArray(value)) {\n value.forEach((element, index) => this.#appendField(fd, `${key}[${index}]`, element))\n } else if (typeof value === \"object\") {\n for (const [subKey, subValue] of Object.entries(value)) {\n this.#appendField(fd, `${key}[${subKey}]`, subValue)\n }\n } else {\n fd.append(key, String(value))\n }\n }\n\n // Names that appear more than once across the chosen files (a `multiple`\n // picker, or several file inputs sharing a name) — those go to params[name][]\n // so the server sees an array; a lone file stays params[name].\n #multiFileNames(files) {\n const counts = new Map()\n for (const { name } of files) counts.set(name, (counts.get(name) ?? 0) + 1)\n return new Set([...counts].filter(([, n]) => n > 1).map(([name]) => name))\n }\n\n #parseParams(raw) {\n if (!raw) return {}\n try {\n return typeof raw === \"string\" ? JSON.parse(raw) : raw\n } catch {\n return {}\n }\n }\n\n // Stimulus typecasts a JSON param to the parsed array already; a raw string\n // (hand-built attr, non-typecasting harness) is parsed here. Delegates to the\n // shared parseOps so the controller and the reactive:js stream action (issue\n // #97) treat a malformed ops attr identically (→ [], never a throw).\n #parseOps(raw) {\n return parseOps(raw)\n }\n\n // Interpret a [[name, args], ...] op list against this root (issue #95),\n // scoping each op's targets to this controller's own root via #opTargets (the\n // nested-reactive-root ownership filter, issue #15). The whitelist + skip\n // logic lives in the shared applyOps so runOps and the reactive:js stream\n // action interpret the SAME vocabulary the SAME way (client-side default-deny).\n #applyOps(list) {\n applyOps(list, (args) => this.#opTargets(args))\n }\n\n // Resolve an op's targets: \"@root\" is this element; a selector resolves\n // WITHIN this root and excludes nested reactive roots' subtrees (issue #15\n // semantics — the same nearest-root ownership check the field walk uses);\n // global: true opts a single op out to document-wide.\n #opTargets(args) {\n const to = args.to\n if (to === \"@root\") return [this.element]\n if (typeof to !== \"string\" || to === \"\") return []\n if (args.global) return [...document.querySelectorAll(to)]\n return [...this.element.querySelectorAll(to)].filter((el) => this.#ownsField(el))\n }\n\n // Whether a click-bound checkbox/radio trigger should keep its NATIVE flip\n // (issue #98). `checked: :keep` means \"let the control flip now\": the\n // unconditional preventDefault is exactly what suppresses that flip until the\n // morph, so we skip it — but ONLY for a checkbox/radio (a bare toggle click\n // has no form-navigation default to lose). Any other element (a button) keeps\n // preventDefault so an in-form submit can't navigate.\n #keepsNativeToggle(optimistic, target) {\n if (optimistic?.checked !== \"keep\") return false\n const type = target?.type\n return type === \"checkbox\" || type === \"radio\"\n }\n\n // Apply the optimistic hint (issue #98) to its targets NOW and return the\n // INVERSE — the exact ops to replay on failure. Cosmetic only: class ops and\n // hidden, applied to the trigger by default or to a `to:` selector scoped to\n // the root. `checked: :keep` records the trigger's post-flip state so a\n // failure snaps the native control back; it applies no DOM change itself (the\n // browser already flipped it). Returns null when there is nothing to do, so\n // the success/failure paths can cheaply skip.\n #applyOptimistic(optimistic, trigger) {\n if (!optimistic) return null\n\n // Class + hidden ops share one target set (trigger, or the `to:` selector).\n const targets = this.#optimisticTargets(optimistic, trigger)\n const undo = []\n for (const el of targets) {\n if (optimistic.add_class) {\n // Undo only the classes this op ACTUALLY added — a class already present\n // was not our change, so reverting it would strip a class the element\n // legitimately had (the add was a no-op). Capture the real delta now.\n const added = optimistic.add_class.filter((c) => !el.classList.contains(c))\n el.classList.add(...added)\n if (added.length) undo.push(() => el.classList.remove(...added))\n }\n if (optimistic.remove_class) {\n // Symmetric: undo only the classes actually removed — one already absent\n // wasn't our change, so re-adding it would introduce a class that wasn't\n // there before.\n const removed = optimistic.remove_class.filter((c) => el.classList.contains(c))\n el.classList.remove(...removed)\n if (removed.length) undo.push(() => el.classList.add(...removed))\n }\n if (optimistic.toggle_class) {\n // toggle_class is its own inverse regardless of prior state — toggling\n // the same classes back exactly restores it, no delta tracking needed.\n optimistic.toggle_class.forEach((c) => el.classList.toggle(c))\n undo.push(() => optimistic.toggle_class.forEach((c) => el.classList.toggle(c)))\n }\n if (optimistic.hide) {\n el.hidden = true\n undo.push(() => (el.hidden = false))\n }\n }\n\n // checked: :keep — the native flip already happened on the trigger; record\n // the inverse (flip it back) so a failure reverts the control's state.\n if (optimistic.checked === \"keep\" && trigger && \"checked\" in trigger) {\n const flipped = trigger.checked\n undo.push(() => (trigger.checked = !flipped))\n }\n\n return undo.length ? undo : null\n }\n\n // Replay the recorded inverse ops on failure (issue #98), guarded by\n // isConnected: a plain (non-morph) replace can detach this subtree before the\n // failure lands, and reverting a stale/detached node is pointless (it's gone)\n // — so a disconnected root skips the revert entirely. On success NOTHING calls\n // this: the server re-render overwrites the hint, or (reply.remove /\n // streams-only) the hint is deliberately left standing.\n #revertOptimistic(inverse) {\n if (!inverse) return\n if (!this.element.isConnected) return\n for (const undo of inverse) undo()\n }\n\n // The elements an optimistic class/hidden hint applies to: the `to:` selector\n // (resolved like an op target — \"@root\" is the root, a selector is scoped to\n // this root's owned matches) or, with no `to:`, the trigger itself.\n #optimisticTargets(optimistic, trigger) {\n if (optimistic.to == null) return trigger ? [trigger] : []\n return this.#opTargets({ to: optimistic.to })\n }\n\n // Apply the loading state for THIS enqueue (issue #99) and return a `settle`\n // closure that undoes exactly this enqueue's contribution when the round trip\n // finishes (success OR any failure). Everything is refcounted so overlapping\n // enqueues never clobber: A's settle can't clear busy while B is still pending.\n //\n // Two layers:\n // 1. The ALWAYS-ON busy vocabulary (fires for every action, no hint needed):\n // data-reactive-busy=\"<action>\" on the trigger and the root (a\n // space-separated, per-action refcounted set), aria-busy on the root (a\n // pending counter), and data-reactive-busy on any busy_on element scoped\n // to this action. Apps style a spinner with pure CSS and zero Ruby.\n // 2. The loading HINT (only when loading:/disable_with: was declared):\n // disable the trigger, add a loading class (to the trigger or a `to:`\n // target), swap its text. These apply at ENQUEUE — never during a debounce\n // quiet period — so a debounced input is not disabled mid-typing.\n #applyLoading(action, trigger, loading) {\n this.#markBusy(action, trigger)\n const restoreHint = this.#applyLoadingHint(action, trigger, loading)\n\n let settled = false\n return () => {\n if (settled) return // one settle per enqueue, even if called twice\n settled = true\n this.#unmarkBusy(action, trigger)\n restoreHint()\n }\n }\n\n // Layer 1 — the always-on busy markers. Trigger + root carry the action token;\n // the root's counter drives aria-busy; busy_on elements scoped to this action\n // light up. Refcounts (#busyActions, #busyPending) so overlapping requests\n // don't clear each other.\n #markBusy(action, trigger) {\n this.#setBusyToken(trigger, action, +1)\n this.#setBusyToken(this.element, action, +1)\n\n this.#busyActions.set(action, (this.#busyActions.get(action) ?? 0) + 1)\n if (this.#busyPending++ === 0) this.element.setAttribute(\"aria-busy\", \"true\")\n\n for (const el of this.#busyOnTargets(action)) this.#setBusyToken(el, action, +1)\n }\n\n #unmarkBusy(action, trigger) {\n this.#setBusyToken(trigger, action, -1)\n this.#setBusyToken(this.element, action, -1)\n\n const count = (this.#busyActions.get(action) ?? 1) - 1\n if (count <= 0) this.#busyActions.delete(action)\n else this.#busyActions.set(action, count)\n\n if (--this.#busyPending <= 0) {\n this.#busyPending = 0\n this.element.removeAttribute(\"aria-busy\")\n }\n\n for (const el of this.#busyOnTargets(action)) this.#setBusyToken(el, action, -1)\n }\n\n // Add (+1) or remove (-1) `action` from an element's space-separated\n // data-reactive-busy token set, refcounted PER ELEMENT+ACTION so two queued\n // requests of the same action on the same element don't drop the token early\n // (and two DIFFERENT actions both keep their token — the set never clobbers).\n // The attribute is removed only when the set empties. No-op on a nullish/\n // detached element (a morph may have replaced the trigger before settle).\n #setBusyToken(el, action, delta) {\n if (!el || typeof el.getAttribute !== \"function\") return\n\n const counts = (this.#busyTokenCounts.get(el) ?? new Map())\n const next = (counts.get(action) ?? 0) + delta\n if (next <= 0) counts.delete(action)\n else counts.set(action, next)\n\n if (counts.size === 0) {\n this.#busyTokenCounts.delete(el)\n el.removeAttribute(\"data-reactive-busy\")\n return\n }\n this.#busyTokenCounts.set(el, counts)\n el.setAttribute(\"data-reactive-busy\", [...counts.keys()].join(\" \"))\n }\n\n // busy_on elements scoped to THIS action, owned by this root (not a nested\n // reactive root's, issue #15). data-reactive-busy-on=\"<action>\" is the marker\n // busy_on(:action) emits.\n #busyOnTargets(action) {\n const nodes = this.element.querySelectorAll?.(\"[data-reactive-busy-on]\") ?? []\n return [...nodes].filter(\n (el) => el.getAttribute(\"data-reactive-busy-on\") === action && this.#ownsField(el),\n )\n }\n\n // Layer 2 — the loading HINT (disable + class + text). Snapshots the trigger's\n // ORIGINAL disabled/text/classes on the FIRST enqueue for that trigger\n // (refcounted so an overlapping enqueue never snapshots the already-swapped\n // \"Saving…\" as the original), applies the swap, and returns a restore closure.\n // With no hint, returns a no-op restore (the always-on busy markers still ran).\n #applyLoadingHint(action, trigger, loading) {\n if (!loading || !trigger) return () => {}\n\n const classTargets = this.#loadingTargets(loading, trigger)\n const classes = Array.isArray(loading.class) ? loading.class : []\n const addedByTarget = []\n for (const el of classTargets) {\n const added = classes.filter((c) => !el.classList.contains(c))\n el.classList.add(...added)\n if (added.length) addedByTarget.push([el, added])\n }\n\n // Snapshot disabled/text ONCE per trigger (refcounted). A second overlapping\n // enqueue increments the count but does NOT re-snapshot — so the recorded\n // \"original\" is the true pre-loading state, never the swapped label.\n const snap = this.#loadingSnapshots.get(trigger)\n if (snap) {\n snap.count++\n } else if (loading.disable || loading.text != null) {\n this.#loadingSnapshots.set(trigger, {\n count: 1,\n disabled: trigger.disabled,\n text: trigger.textContent,\n hadText: loading.text != null,\n })\n }\n\n if (loading.disable) trigger.disabled = true\n if (loading.text != null) trigger.textContent = loading.text\n\n return () => {\n for (const [el, added] of addedByTarget) if (el.isConnected) el.classList.remove(...added)\n this.#restoreLoadingSnapshot(trigger, loading)\n }\n }\n\n // Restore the trigger's disabled/text from its snapshot when the LAST enqueue\n // for that trigger settles (refcount → 0). GUARDED: skip a disconnected\n // trigger (a plain replace detached it — the node is gone), and do NOT restore\n // the text if it no longer equals what we swapped IN (a morph rendered a new\n // server label — clobbering it with the old text would fight server truth).\n #restoreLoadingSnapshot(trigger, loading) {\n const snap = this.#loadingSnapshots.get(trigger)\n if (!snap) return\n if (--snap.count > 0) return // another enqueue for this trigger is still pending\n this.#loadingSnapshots.delete(trigger)\n\n if (!trigger.isConnected) return // detached — nothing to restore\n\n if (loading.disable) trigger.disabled = snap.disabled\n // Only restore the label if the trigger still shows OUR swapped text; a\n // changed textContent means the server morph relabeled it — leave it.\n if (snap.hadText && trigger.textContent === loading.text) trigger.textContent = snap.text\n }\n\n // The elements a loading class applies to: the `to:` selector (resolved like\n // an op target — \"@root\" is the root, a selector is scoped to this root's\n // owned matches) or, with no `to:`, the trigger itself.\n #loadingTargets(loading, trigger) {\n if (loading.to == null) return trigger ? [trigger] : []\n return this.#opTargets({ to: loading.to })\n }\n\n // The action path comes from a <meta> tag that is fixed for the page's life,\n // so resolve it once per controller and cache it — avoids a querySelector on\n // every dispatch (this runs on the request hot path, once per click/keystroke\n // round trip). Cached on the instance, so a fresh connect() (after a Turbo\n // navigation swaps the element) re-reads it.\n #actionPath() {\n return (this.#actionPathCache ??=\n document.querySelector('meta[name=\"phlex-reactive-action-path\"]')?.content ||\n \"/reactive/actions\")\n }\n\n // The per-request timeout in ms (issue #101), from a page-stable\n // <meta name=\"phlex-reactive-timeout\"> (default 30000). Cached per-controller\n // like the action path. Parsed defensively: a missing/blank/non-positive/NaN\n // value falls back to the default, so a typo'd meta can never disable the\n // timeout (which would reintroduce the wedged-queue bug) or set a zero/negative\n // window that aborts instantly.\n #timeoutMs() {\n if (this.#timeoutMsCache != null) return this.#timeoutMsCache\n const raw = document.querySelector('meta[name=\"phlex-reactive-timeout\"]')?.content\n const ms = Number(raw)\n return (this.#timeoutMsCache = Number.isFinite(ms) && ms > 0 ? ms : 30000)\n }\n\n // CSRF token and connection id are read LIVE (not cached) on purpose: Rails\n // can rotate the CSRF token, and the pgbus connection id changes on an SSE\n // reconnect — caching either would send a stale value. A single querySelector\n // per request is cheap next to the round trip itself.\n #csrfToken() {\n return document.querySelector('meta[name=\"csrf-token\"]')?.content ?? \"\"\n }\n\n // The pgbus SSE connection id, if the page is subscribed to a stream. pgbus\n // reflects it onto the <pgbus-stream-source connection-id=\"…\"> element (and\n // apps may mirror it to <meta name=\"pgbus-connection-id\">). Returns null\n // when not present (e.g. no pgbus, or no active subscription) — the header\n // is then simply omitted.\n #connectionId() {\n return (\n document.querySelector(\"pgbus-stream-source[connection-id]\")?.getAttribute(\"connection-id\") ||\n document.querySelector('meta[name=\"pgbus-connection-id\"]')?.content ||\n null\n )\n }\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "CA27DF71E8491EB164756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "B4ECABAC7819655164756E2164756E21",
9
9
  "names": []
10
10
  }