phlex-reactive 0.13.4 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1050 -630
  3. data/README.md +561 -14
  4. data/app/controllers/phlex/reactive/actions_controller.rb +85 -16
  5. data/app/controllers/phlex/reactive/fragments_controller.rb +310 -0
  6. data/app/javascript/phlex/reactive/core.js +181 -0
  7. data/app/javascript/phlex/reactive/core.min.js +4 -0
  8. data/app/javascript/phlex/reactive/core.min.js.map +11 -0
  9. data/app/javascript/phlex/reactive/early.js +175 -0
  10. data/app/javascript/phlex/reactive/early.min.js +4 -0
  11. data/app/javascript/phlex/reactive/early.min.js.map +10 -0
  12. data/app/javascript/phlex/reactive/features/bindings.js +1511 -0
  13. data/app/javascript/phlex/reactive/features/bindings.min.js +4 -0
  14. data/app/javascript/phlex/reactive/features/bindings.min.js.map +10 -0
  15. data/app/javascript/phlex/reactive/features/compute.js +541 -0
  16. data/app/javascript/phlex/reactive/features/compute.min.js +4 -0
  17. data/app/javascript/phlex/reactive/features/compute.min.js.map +10 -0
  18. data/app/javascript/phlex/reactive/features/defer.js +726 -0
  19. data/app/javascript/phlex/reactive/features/defer.min.js +4 -0
  20. data/app/javascript/phlex/reactive/features/defer.min.js.map +10 -0
  21. data/app/javascript/phlex/reactive/features/devtools.js +242 -0
  22. data/app/javascript/phlex/reactive/features/devtools.min.js +4 -0
  23. data/app/javascript/phlex/reactive/features/devtools.min.js.map +10 -0
  24. data/app/javascript/phlex/reactive/features/effects.js +362 -0
  25. data/app/javascript/phlex/reactive/features/effects.min.js +4 -0
  26. data/app/javascript/phlex/reactive/features/effects.min.js.map +10 -0
  27. data/app/javascript/phlex/reactive/features/form.js +216 -0
  28. data/app/javascript/phlex/reactive/features/form.min.js +4 -0
  29. data/app/javascript/phlex/reactive/features/form.min.js.map +10 -0
  30. data/app/javascript/phlex/reactive/features/hints.js +193 -0
  31. data/app/javascript/phlex/reactive/features/hints.min.js +4 -0
  32. data/app/javascript/phlex/reactive/features/hints.min.js.map +10 -0
  33. data/app/javascript/phlex/reactive/features/persist.js +663 -0
  34. data/app/javascript/phlex/reactive/features/persist.min.js +4 -0
  35. data/app/javascript/phlex/reactive/features/persist.min.js.map +10 -0
  36. data/app/javascript/phlex/reactive/reactive_controller.js +85 -5821
  37. data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
  38. data/app/javascript/phlex/reactive/reactive_controller.min.js.map +13 -4
  39. data/app/javascript/phlex/reactive/runtime.js +3690 -0
  40. data/lib/generators/phlex/reactive/install/templates/phlex_reactive.rb.erb +18 -0
  41. data/lib/phlex/reactive/claude/skills/phlex-reactive-debugging/SKILL.md +5 -3
  42. data/lib/phlex/reactive/component/dsl.rb +74 -2
  43. data/lib/phlex/reactive/component/helpers.rb +39 -9
  44. data/lib/phlex/reactive/component/lazy.rb +138 -2
  45. data/lib/phlex/reactive/component/registry.rb +2 -0
  46. data/lib/phlex/reactive/component/selection.rb +71 -0
  47. data/lib/phlex/reactive/doctor/fiber_isolation.rb +86 -0
  48. data/lib/phlex/reactive/doctor.rb +166 -8
  49. data/lib/phlex/reactive/dormant.rb +66 -0
  50. data/lib/phlex/reactive/engine.rb +63 -3
  51. data/lib/phlex/reactive/fragment.rb +233 -0
  52. data/lib/phlex/reactive/inspector/report.rb +24 -3
  53. data/lib/phlex/reactive/inspector.rb +23 -2
  54. data/lib/phlex/reactive/js.rb +17 -2
  55. data/lib/phlex/reactive/mcp/tools/actions_tool.rb +3 -1
  56. data/lib/phlex/reactive/mcp/tools/find_tool.rb +4 -1
  57. data/lib/phlex/reactive/param_schema.rb +68 -54
  58. data/lib/phlex/reactive/show_conditions.rb +84 -56
  59. data/lib/phlex/reactive/streamable.rb +20 -2
  60. data/lib/phlex/reactive/test_helpers/system/reactive_requests_matcher.rb +120 -0
  61. data/lib/phlex/reactive/test_helpers/system.rb +62 -0
  62. data/lib/phlex/reactive/version.rb +1 -1
  63. data/lib/phlex/reactive.rb +84 -0
  64. data/lib/tasks/phlex_reactive.rake +6 -2
  65. metadata +38 -1
@@ -0,0 +1,4 @@
1
+ var y;function re(e){y=e}var s=new Map;function M(e,t){let n=!s.has(e);if(s.set(e,t),n)y.enter()}function w(e){if(s.delete(e))y.exit()}function ne(){s.clear(),k=!1}function ie(e){return s.get(e)?.via}var k=!1;function oe(e){O();let t=e.getAttribute("target");if(!t)return;if(e.getAttribute("data-reactive-defer-via")==="stream"){j(t,e);return}let n=e.getAttribute("data-reactive-defer-token");if(!n)return;v(t,n)}function O(){if(k||typeof document>"u"||!document.addEventListener)return;k=!0,document.addEventListener("turbo:before-stream-render",J,!0),document.addEventListener("turbo:before-stream-render",U)}function U(e){let t=e.target?.getAttribute?.("target");if(!t?.startsWith("reactive-defer-src-"))return;let n=t.slice(19);if(s.get(n)?.via==="stream")w(n)}function v(e,t){let n=document.getElementById(e);if(!n){console.warn(`[phlex-reactive] reactive:defer target #${e} is not on the page — skipped`);return}S(e),N(n);let r={via:"fetch",abort:new AbortController,timedOut:!1};return M(e,r),H(e,r,t)}function _(e,t){if(typeof e!=="string"){let n={headers:{Accept:"text/vnd.turbo-stream.html"},credentials:"same-origin",signal:t};if(e.fallback)n.cache="no-store";return[e.src,n]}return[X(),{method:"POST",headers:{Accept:"text/vnd.turbo-stream.html","Content-Type":"application/json","X-CSRF-Token":Y()},body:JSON.stringify({token:e}),credentials:"same-origin",signal:t}]}function C(e){let t=e.getAttribute?.("data-reactive-defer-src");if(!t)return;if(F(t))return{src:t};return console.error(V(t)),W(t)||Z(e)}function V(e){return`[phlex-reactive] refused data-reactive-defer-src="${e}" — it is not under this app's fragment path `+`(${E()}). If you changed Phlex::Reactive.fragment_path, add `+'<meta name="phlex-reactive-fragment-path" content="…your path…"> to the layout <head>.'}function W(e){let t;try{t=new URL(e,window.location.href)}catch{return}let n=t.pathname.split("/").pop(),r=`${E()}/${n}${t.search}`;if(/^[\w-]+$/.test(n)&&F(r))return{src:r,fallback:!0}}function Z(e){S(e.id),D(e),e.setAttribute("data-reactive-error","defer"),e.dispatchEvent(new CustomEvent("reactive:error",{bubbles:!0,composed:!0,detail:{kind:"defer",target:e.id,reason:"refused-url"}}))}function F(e){try{let t=new URL(window.location.href),n=new URL(e,t);return n.origin===t.origin&&n.pathname.startsWith(`${E()}/`)}catch{return!1}}function E(){return document.head?.querySelector?.('meta[name="phlex-reactive-fragment-path"]')?.content||"/reactive/fragment"}function j(e,t){let n=document.getElementById(e);if(!n){console.warn(`[phlex-reactive] reactive:defer target #${e} is not on the page — skipped`);return}let r=t.getAttribute("data-reactive-defer-src");if(!r)return;if(!globalThis.customElements?.get?.("pgbus-stream-source")){let o=t.getAttribute("data-reactive-defer-token");if(o){v(e,o);return}console.error("[phlex-reactive] reactive:defer via=stream but <pgbus-stream-source> is not registered "+"and no fallback token was provided — is the pgbus client loaded on this page?");return}S(e),N(n);let i=document.createElement("pgbus-stream-source");i.id=z(e),i.setAttribute("src",r),i.setAttribute("since-id",t.getAttribute("data-reactive-defer-since-id")??"0"),i.setAttribute("hidden",""),document.body.appendChild(i),M(e,{via:"stream"})}function z(e){return`reactive-defer-src-${e}`}async function H(e,t,n){let r=setTimeout(()=>{t.timedOut=!0,t.abort.abort()},G()),i;try{y.count("defer"),i=await fetch(..._(n,t.abort.signal))}catch(u){if(clearTimeout(r),s.get(e)!==t)return;console.error("[phlex-reactive] deferred render failed",u),m(e,n);return}if(s.get(e)!==t){clearTimeout(r);return}if(i.redirected){clearTimeout(r),console.error(`[phlex-reactive] deferred render failed: redirected to ${i.url??"another URL"}`),m(e,n,i.status);return}if(i.status===204){clearTimeout(r),R(e);return}if(!i.ok){clearTimeout(r),console.error(`[phlex-reactive] deferred render failed: HTTP ${i.status}`),m(e,n,i.status);return}let o=i.headers?.get?.("Content-Type")||"";if(!o.includes("turbo-stream")){clearTimeout(r),console.error(`[phlex-reactive] deferred render failed: expected a turbo-stream, got "${o}"`),m(e,n,i.status);return}let f;try{f=await i.text()}catch(u){if(clearTimeout(r),s.get(e)!==t)return;console.error("[phlex-reactive] deferred render failed reading the body",u),m(e,n);return}if(clearTimeout(r),s.get(e)!==t)return;R(e),K(f)}var T=null,J=(e)=>T?.push(e);function K(e){let t=T=[];try{window.Turbo.renderStreamMessage(e)}finally{T=null}for(let n of t){if(n.defaultPrevented)continue;let r=n.detail,i=r.render;r.render=()=>{},new Promise((o)=>o(i(r.newStream))).catch(console.error)}}function S(e){let t=s.get(e);if(!t)return;if(w(e),t.via==="fetch")t.abort.abort();else document.getElementById(z(e))?.remove?.()}function N(e){e.setAttribute("data-reactive-defer-pending","true"),e.setAttribute("aria-busy","true")}function D(e){e.removeAttribute("data-reactive-defer-pending"),e.removeAttribute("aria-busy")}function R(e){w(e);let t=document.getElementById(e);if(!t)return;D(t),t.removeAttribute("data-reactive-error")}function m(e,t,n){w(e);let r=document.getElementById(e);if(!r)return;D(r),r.setAttribute("data-reactive-error","defer");let i=()=>{let o=document.getElementById(e);if(!o){console.warn("[phlex-reactive] defer retry() ignored — the target left the DOM");return}o.removeAttribute("data-reactive-error"),v(e,t)};r.dispatchEvent(new CustomEvent("reactive:error",{bubbles:!0,composed:!0,detail:{kind:"defer",target:e,status:n,retry:i}}))}function X(){return document.querySelector('meta[name="phlex-reactive-defer-path"]')?.content||"/reactive/defer"}function Y(){return document.querySelector('meta[name="csrf-token"]')?.content??""}function G(){let e=document.querySelector('meta[name="phlex-reactive-timeout"]')?.content,t=Number(e);return Number.isFinite(t)&&t>0?t:30000}var q="__materialize",A=new WeakMap;function g(e){if(e.getAttribute?.("data-reactive-lazy-visible")!=null)return"visible";if(e.getAttribute?.("data-reactive-lazy-on"))return"event";return null}function ae(e,t,n){O();let r=e.element,i=!1,o=!1,f=null,u=null,d,b,l=()=>Q(e),L=(a)=>{if(g(r)==="visible"){if(!f)B()}else if(a){let c=r.getAttribute("data-reactive-lazy-on");if(u===c)return;h(),r.addEventListener?.(c,l),u=c}},h=()=>{if(f?.disconnect(),f=null,u)r.removeEventListener?.(u,l);u=null},B=()=>{if(typeof IntersectionObserver>"u")return queueMicrotask(l);let a=r.getAttribute("data-reactive-lazy-visible")||"0px";f=new IntersectionObserver((c)=>{if(c.some((p)=>p.isIntersecting))l()},{rootMargin:a}),f.observe(r)},P=()=>{let a=g(r)!==null,c=i;if(i=a,!a)return h();if(o)return;if(t.forgetToken(),!c)return l();L(!0)};if(A.set(e,{load(){if(o)return;let a=C(r);if(!a&&r.getAttribute("data-reactive-defer-src")!=null)return;if(a&&I(r,t))return;let c=a?v(r.id,a):t.proceed(r,q,"{}");if(!c)return;o=!0,h();let p=()=>{o=!1};return c.then(p,p),c},off(){if(d)r.removeEventListener?.("turbo:morph-element",d);if(b)r.removeEventListener?.("turbo:morph-element",b);h()}}),(r.getAttribute?.("data-reactive-defer-token")||r.getAttribute?.("data-reactive-defer-src"))&&g(r)===null)x(r),d=(a)=>a.target===r&&x(r),r.addEventListener?.("turbo:morph-element",d);if(r.getAttribute?.("data-reactive-token-value")==null&&r.getAttribute?.("data-reactive-defer-src")==null)return;if(b=(a)=>{if(a.target!==r)return;if(!d)x(r);P()},r.addEventListener?.("turbo:morph-element",b),n)return P();if(i=g(r)!==null,i)L(!1)}function ce(e){let t=A.get(e);A.delete(e),t?.off()}function Q(e){return A.get(e)?.load()}function I(e,t){return t.emit("reactive:before-dispatch",{action:q,params:{},element:e},{cancelable:!0}).defaultPrevented}function x(e){if(!e?.id)return;if(e.getAttribute?.("data-reactive-defer-pending")!=="true")return;let t=e.getAttribute?.("data-reactive-defer-token")||C(e);if(t)v(e.id,t)}export{q as LAZY_MATERIALIZE_ACTION,ae as connect,ce as disconnect,re as install,Q as materialize,ie as pendingDeferVia,ne as resetReactiveDefers,oe as streamAction};
2
+
3
+ //# debugId=F196EE72B0069C0364756E2164756E21
4
+ //# sourceMappingURL=defer.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["features/defer.js"],
4
+ "sourcesContent": [
5
+ "// Deferred and lazy renders — a FEATURE MODULE of the reactive controller\n// (issue #275). The core imports it only when a root on the page is a\n// reactive_lazy shell (it carries data-reactive-defer-token, -defer-src,\n// -lazy-on or -lazy-visible), when a morph turns a root into one, or when a\n// `reactive:defer` stream arrives.\n//\n// Three things live here:\n//\n// reply.defer (issue #165) the client half: 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\n// a pgbus one-shot stream (push).\n// reactive_lazy (issue #165) a shell that fetches its real content when it\n// connects; with cache: (issue #277) from a stable, privately cacheable URL.\n// reactive_lazy(on:) (issue #276) a shell that loads on an event, or when it\n// scrolls into view.\n//\n// Everything about a delivery is MODULE-level, deliberately OFF the\n// per-controller request queue: the whole point is that the expensive segment\n// 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.\n//\n// The feature contract (reactive_controller.js \"Feature modules\"):\n// install(shared) once, when the module has loaded\n// connect(controller, core, morphed) per root connection\n// disconnect(controller) before the core's own teardown\n// A feature never imports the core. `shared` carries the core's single-\n// instance counters (the activity signal, the request totals); `core` is the\n// per-controller handle: core.emit, core.proceed (the action pipeline) and\n// core.forgetToken.\n\n// The core's module-level counters, handed over once by install(): there must\n// be ONE activity count and one request total on a page, whoever adds to them.\nlet shared\n\nexport function install(given) {\n shared = given\n}\n\nconst pendingDefers = new Map()\n\n// Register/drop a target's pending defer entry, keeping the GLOBAL activity\n// counter (issue #201) balanced against the Map's ACTUAL key presence — a defer\n// is one in-flight reactive operation for as long as its registry entry lives.\n// Keying enter/exit on the presence TRANSITION (not the raw call) means a\n// re-set of an already-present key, or a delete of an absent one, can never\n// unbalance the count. Both the fetch (pull) and stream (push) lane route their\n// registry mutations through here, so the push lane is counted correctly even\n// though its DOM pending markers clear by a node swap, not clearDeferPending.\nfunction setPendingDefer(targetId, entry) {\n const isNew = !pendingDefers.has(targetId)\n pendingDefers.set(targetId, entry)\n if (isNew) shared.enter()\n}\n\nfunction deletePendingDefer(targetId) {\n if (pendingDefers.delete(targetId)) shared.exit()\n}\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. Does NOT touch the activity counter —\n// resetReactiveActivity is its own seam (the two are reset together in tests).\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\n// The body of the `reactive:defer` Turbo stream action. The core registers the\n// action itself (a stream can arrive on a page that has not loaded this module\n// yet) and hands each <turbo-stream> element over once the module is here —\n// its attributes outlive Turbo removing the element from the document.\nexport function streamAction(streamEl) {\n registerSettleOnRender()\n const target = streamEl.getAttribute(\"target\")\n if (!target) return\n if (streamEl.getAttribute(\"data-reactive-defer-via\") === \"stream\") {\n startStreamDefer(target, streamEl)\n return\n }\n const token = streamEl.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 then removes the\n// source. A document-level turbo:before-stream-render hook drops the Map\n// entry the moment that removal renders — so the entry never outlives the\n// delivery (the fetch lane settles inline; the stream lane's arrival is a\n// broadcast nothing here awaits, so it needs this hook). Registered once; a\n// no-op without document.\nfunction registerSettleOnRender() {\n if (deferStreamSettleRegistered || typeof document === \"undefined\" || !document.addEventListener) return\n deferStreamSettleRegistered = true\n // Capture phase: seen before any listener can stop it (renderStreamsNow).\n document.addEventListener(\"turbo:before-stream-render\", catchStream, true)\n document.addEventListener(\"turbo:before-stream-render\", settleStreamDeferOnRender)\n}\n\n// Drop a stream-lane pendingDefers entry when a turbo-stream targets its\n// source element, reactive-defer-src-<target>: only the job's broadcast does\n// (every payload, render or cleanup, ends with that removal). A stream to the\n// target ITSELF settles nothing (issue #292): an unrelated update of #<target>\n// between the enqueue and the job's broadcast is not the delivery. Pure Map\n// cleanup — the DOM apply is Turbo's; this only releases our bookkeeping.\nfunction settleStreamDeferOnRender(event) {\n const target = event.target?.getAttribute?.(\"target\")\n if (!target?.startsWith(\"reactive-defer-src-\")) return\n const targetId = target.slice(\"reactive-defer-src-\".length)\n if (pendingDefers.get(targetId)?.via === \"stream\") deletePendingDefer(targetId)\n}\n\n// The pull lane: mark the target pending and fetch the real render, in\n// parallel with everything else the page is doing. `source` is what to fetch:\n// a signed defer token (a string — POSTed to the defer endpoint) or `{ src }`,\n// a `reactive_lazy cache:` component's stable fragment URL (issue #277) —\n// fetched with a plain GET so the browser's private HTTP cache can answer it.\n// Returns the fetch's promise (it never rejects), or undefined when skipped.\nfunction startFetchDefer(targetId, source) {\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 setPendingDefer(targetId, entry)\n return performDeferFetch(targetId, entry, source)\n}\n\n// The fetch() arguments for a pull-lane source. A fragment URL is a GET with\n// no CSRF token and no body: rendering has no side effects, and anything that\n// varied per request would defeat the cache (the response is keyed on the URL,\n// Vary: Cookie). The default cache mode is what we want — reuse a fresh copy,\n// revalidate a stale one with its ETag. A fallback GET (see\n// fallbackFragmentSource) is the exception: the server never issued that URL,\n// so its reply is kept out of the HTTP cache.\nfunction deferRequest(source, signal) {\n if (typeof source !== \"string\") {\n const init = { headers: { Accept: \"text/vnd.turbo-stream.html\" }, credentials: \"same-origin\", signal }\n if (source.fallback) init.cache = \"no-store\"\n return [source.src, init]\n }\n return [\n deferPath(),\n {\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: source }),\n credentials: \"same-origin\",\n signal,\n },\n ]\n}\n\n// A `cache:` shell's fragment URL as a pull-lane source, or undefined (no\n// URL). Unlike a token (opaque, POSTed to one fixed path) this is a URL read\n// from the DOM, and its response is rendered as a turbo-stream — so it is\n// fetched ONLY when it resolves to this origin's fragment endpoint. Markup that\n// smuggled the attribute in (user HTML that kept data-* attributes) must not be\n// able to point the client at another origin, an upload, or any other\n// same-origin path. A refused URL is logged and loads through the fallback GET\n// below (issue #306); with no fallback the shell is failed, and this answers\n// undefined.\nfunction fragmentSource(el) {\n const src = el.getAttribute?.(\"data-reactive-defer-src\")\n if (!src) return\n if (isFragmentUrl(src)) return { src }\n console.error(refusedFragmentMessage(src))\n return fallbackFragmentSource(src) || failRefusedFragment(el)\n}\n\nfunction refusedFragmentMessage(src) {\n return (\n `[phlex-reactive] refused data-reactive-defer-src=\"${src}\" — it is not under this app's fragment path ` +\n `(${fragmentPath()}). If you changed Phlex::Reactive.fragment_path, add ` +\n '<meta name=\"phlex-reactive-fragment-path\" content=\"…your path…\"> to the layout <head>.'\n )\n}\n\n// The way a shell with a refused URL still loads (issue #306): a GET of the\n// SAME signed id under the fragment path this client knows — so the request\n// can only ever reach this app's own fragment endpoint, which verifies the id.\n// It catches an app that renders absolute fragment URLs for another host\n// name, or moved fragment_path without the meta (then the GET answers 404 and\n// the shell fails with retry(), like any failed load). Undefined when the\n// URL's last path segment cannot be a fragment id.\nfunction fallbackFragmentSource(src) {\n let url\n try {\n url = new URL(src, window.location.href)\n } catch {\n return\n }\n const id = url.pathname.split(\"/\").pop()\n const fallback = `${fragmentPath()}/${id}${url.search}`\n // Held to the same check as any fragment URL, whatever the meta says.\n if (/^[\\w-]+$/.test(id) && isFragmentUrl(fallback)) return { src: fallback, fallback: true }\n}\n\n// A shell whose URL was refused and has no fallback has no other way to load,\n// so it must not shimmer forever: clear pending, mark the root, and emit the same\n// bubbling reactive:error a failed load does (no retry() — the URL won't change).\n// It supersedes whatever load is in flight for this root first (issue #293):\n// that load was for the shell this morph replaced, and its late arrival must\n// neither paint over this one nor clear its error marker.\nfunction failRefusedFragment(el) {\n supersedeDefer(el.id)\n clearDeferPending(el)\n el.setAttribute(\"data-reactive-error\", \"defer\")\n el.dispatchEvent(\n new CustomEvent(\"reactive:error\", {\n bubbles: true,\n composed: true,\n detail: { kind: \"defer\", target: el.id, reason: \"refused-url\" },\n }),\n )\n}\n\nfunction isFragmentUrl(src) {\n try {\n const here = new URL(window.location.href)\n const url = new URL(src, here)\n return url.origin === here.origin && url.pathname.startsWith(`${fragmentPath()}/`)\n } catch {\n return false\n }\n}\n\n// Phlex::Reactive.fragment_path, for the check above. An app that moves the\n// endpoint renders <meta name=\"phlex-reactive-fragment-path\"> (as for the\n// action and defer paths); the URL itself always comes from the shell. Read\n// from <head> ONLY: this meta widens what the client will fetch and render, so\n// one injected into the body must not count.\nfunction fragmentPath() {\n return document.head?.querySelector?.('meta[name=\"phlex-reactive-fragment-path\"]')?.content || \"/reactive/fragment\"\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 removes the source (a\n // turbo:before-stream-render hook, below).\n setPendingDefer(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, source) {\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 // Counted per fetch() CALL — a fragment GET the browser answers from its\n // HTTP cache still counts (it is a request the client issued; whether it\n // touched the network is Resource Timing's transferSize).\n shared.count(\"defer\")\n response = await fetch(...deferRequest(source, entry.abort.signal))\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, source)\n return\n }\n if (pendingDefers.get(targetId) !== entry) {\n clearTimeout(timer)\n return // superseded mid-flight\n }\n\n // The body is about to be rendered as a turbo-stream, so it must BE one, from\n // the URL we asked for. A base-controller filter that redirects to a sign-in\n // page ends in a 200 HTML document — a failed load, never rendered.\n if (response.redirected) {\n clearTimeout(timer)\n console.error(`[phlex-reactive] deferred render failed: redirected to ${response.url ?? \"another URL\"}`)\n failDefer(targetId, source, response.status)\n return\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, source, response.status)\n return\n }\n const contentType = response.headers?.get?.(\"Content-Type\") || \"\"\n if (!contentType.includes(\"turbo-stream\")) {\n clearTimeout(timer)\n console.error(`[phlex-reactive] deferred render failed: expected a turbo-stream, got \"${contentType}\"`)\n failDefer(targetId, source, 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, source)\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 renderStreamsNow(html)\n}\n\n// Render a reply's streams in this task, not a frame from now (issue #336).\n// renderStreamMessage appends the <turbo-stream>s; each one dispatches\n// turbo:before-stream-render as it connects, then awaits nextRepaint() before\n// calling event.detail.render(this). The events are caught here, and once\n// every listener has had its say (dispatch is synchronous) the final\n// detail.render runs now, in order, unless the event was cancelled; Turbo's\n// frame-later call gets a no-op. Everything else — the event, the parse,\n// scripts, permanent elements, autofocus — stays Turbo's. A Turbo that\n// dispatches later is never caught, and renders on its own as before.\n// The catching listener is registerSettleOnRender's (every fetch starts after\n// it ran).\nlet caughtStreams = null\nconst catchStream = (event) => caughtStreams?.push(event)\n\nfunction renderStreamsNow(html) {\n const caught = (caughtStreams = [])\n try {\n window.Turbo.renderStreamMessage(html)\n } finally {\n caughtStreams = null\n }\n for (const event of caught) {\n if (event.defaultPrevented) continue\n const detail = event.detail\n const render = detail.render\n detail.render = () => {}\n // A throwing render is logged, as Turbo's connectedCallback does, and the\n // next stream still applies.\n new Promise((resolve) => resolve(render(detail.newStream))).catch(console.error)\n }\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 deletePendingDefer(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 deletePendingDefer(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 source\n// (a token is still valid inside the TTL; an expired one 400s into this same\n// path. A fragment URL never expires).\nfunction failDefer(targetId, source, status) {\n deletePendingDefer(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, source)\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// --- Per-root wiring -------------------------------------------------------\n\nexport const LAZY_MATERIALIZE_ACTION = \"__materialize\"\n\n// controller -> this connection's { load, off }: its materialize and its\n// teardown, closures over the connection's lazy state (locals of connect(),\n// not a record's fields — the minifier renames a local, never a property,\n// issue #310):\n// wasShell whether the root was a trigger shell when last looked at\n// (connect, then every morph) — a morph that turns REAL content\n// back into a shell re-materializes; one that leaves a shell a\n// shell only re-arms it.\n// inFlight the ONE dedupe point every materialize passes (the Stimulus\n// binding, the observer, the re-armed listener, a morph-back).\n// observer, eventName, onProbe, onMorph held for teardown.\nconst wired = new WeakMap()\n\n// Which reactive_lazy(on:) shell this root currently is — read live, because\n// a morph rewrites the attributes on a connected element. null = real content\n// (or a fetch-on-connect shell).\nfunction shellKind(el) {\n if (el.getAttribute?.(\"data-reactive-lazy-visible\") != null) return \"visible\"\n if (el.getAttribute?.(\"data-reactive-lazy-on\")) return \"event\"\n return null\n}\n\n// `morphed` is true when this connect follows a MORPH of an already connected\n// root (the core re-scans for feature markers on a morph of the root): the\n// root showed real content and the morph turned it into a shell.\nexport function connect(controller, core, morphed) {\n registerSettleOnRender()\n const el = controller.element\n let wasShell = false\n let inFlight = false\n let observer = null\n let eventName = null\n let onProbe\n let onMorph\n // Every trigger goes through the exported materialize, so one that fires\n // after disconnect (a queued microtask) finds nothing wired and does nothing.\n const onEvent = () => materialize(controller)\n\n // Arm the shell's trigger. A :visible shell gets (at most one) observer. An\n // event shell's FIRST event rides its Stimulus `once` binding, so on connect\n // there is nothing to add; after a morph (`rearm`) that binding may be spent\n // — Stimulus only re-binds when the descriptor attribute itself changed — so\n // this listens for the event itself. Both can fire for one event; materialize\n // dedupes. Either trigger is consumed when a request starts (like `once`):\n // one morph buys one attempt.\n const arm = (rearm) => {\n if (shellKind(el) === \"visible\") {\n if (!observer) observeVisible()\n } else if (rearm) {\n const name = el.getAttribute(\"data-reactive-lazy-on\")\n if (eventName === name) return\n disarm()\n el.addEventListener?.(name, onEvent)\n eventName = name\n }\n }\n\n const disarm = () => {\n observer?.disconnect()\n observer = null\n if (eventName) el.removeEventListener?.(eventName, onEvent)\n eventName = null\n }\n\n // reactive_lazy(on: :visible) (issue #276): materialize the first time the\n // shell intersects the viewport (grown or shrunk by the rendered\n // rootMargin). Without IntersectionObserver (very old engines) materialize\n // right away: the content still loads, just not lazily.\n const observeVisible = () => {\n if (typeof IntersectionObserver === \"undefined\") return queueMicrotask(onEvent)\n const rootMargin = el.getAttribute(\"data-reactive-lazy-visible\") || \"0px\"\n observer = new IntersectionObserver(\n (entries) => {\n if (entries.some((entry) => entry.isIntersecting)) onEvent()\n },\n { rootMargin },\n )\n observer.observe(el)\n }\n\n // turbo:morph-element OF the root: the morph is server truth arriving on a\n // CONNECTED element. Four outcomes:\n // * now real content → nothing to load; drop any armed trigger.\n // * a load is in flight → leave it; its reply replaces the shell.\n // * was real, now a shell → it was loaded and the morph wiped it, so\n // re-materialize now (for an event shell\n // the event — a panel opening — already\n // happened and won't fire again).\n // * was a shell, still a shell → never triggered, or a failed load: re-arm\n // it (this is a failed load's retry path).\n const afterMorph = () => {\n const shell = shellKind(el) !== null\n const was = wasShell\n wasShell = shell\n if (!shell) return disarm()\n if (inFlight) return\n // The morphed-in shell's token is the page's current identity; a token\n // cached from an earlier reply would materialize stale state.\n core.forgetToken()\n if (!was) return onEvent()\n arm(true)\n }\n\n wired.set(controller, {\n // See materialize() below.\n load() {\n if (inFlight) return\n const source = fragmentSource(el)\n // A URL refused with no fallback has already failed the shell: never POST.\n if (!source && el.getAttribute(\"data-reactive-defer-src\") != null) return\n // The GET skips the action pipeline, so raise its veto here: an app's\n // reactive:before-dispatch listener controls a materialize either way.\n if (source && materializeVetoed(el, core)) return\n const run = source ? startFetchDefer(el.id, source) : core.proceed(el, LAZY_MATERIALIZE_ACTION, \"{}\")\n if (!run) return // vetoed by reactive:before-dispatch (or the root has no id)\n inFlight = true\n // Consume the armed trigger (observer or re-armed listener): a failed load\n // is retried by the NEXT morph, not by every later event.\n disarm()\n const done = () => {\n inFlight = false\n }\n run.then(done, done)\n return run\n },\n off() {\n if (onProbe) el.removeEventListener?.(\"turbo:morph-element\", onProbe)\n if (onMorph) el.removeEventListener?.(\"turbo:morph-element\", onMorph)\n disarm()\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. turbo:morph-element BUBBLES, and a re-probe\n // supersedes (aborts and re-issues) the fetch in flight, so only a morph of\n // the root itself counts (issue #294). The attribute stays on the shell\n // precisely so a re-appearance re-fires.\n // A `cache:` shell (issue #277) carries a fragment URL instead of the token\n // and takes the same path; an on: shell with a URL is NOT probed — it waits\n // for its trigger (its root has no pending marker).\n const fetches = el.getAttribute?.(\"data-reactive-defer-token\") || el.getAttribute?.(\"data-reactive-defer-src\")\n if (fetches && shellKind(el) === null) {\n probe(el)\n onProbe = (event) => event.target === el && probe(el)\n el.addEventListener?.(\"turbo:morph-element\", onProbe)\n }\n\n // reactive_lazy(on:) shells (issue #276) carry NO defer token, so the probe\n // above skips them: an event shell waits for its once-bound __materialize\n // trigger, a :visible shell for the observer armed here. An on: shell\n // carries either the identity token or (with cache:, issue #306) its\n // fragment URL; a root with neither is client-only, never an on: shell.\n const tokenless = el.getAttribute?.(\"data-reactive-token-value\") == null\n if (tokenless && el.getAttribute?.(\"data-reactive-defer-src\") == null) return\n // turbo:morph-element BUBBLES: only a morph of the root itself counts, never\n // one of a descendant (a morphed skeleton child, a nested root). The same\n // morph can turn real content into a FETCH-ON-CONNECT shell (plain\n // reactive_lazy, or cache: without on:): probe it too, unless this root\n // connected as such a shell and already re-probes on every morph.\n onMorph = (event) => {\n if (event.target !== el) return\n if (!onProbe) probe(el)\n afterMorph()\n }\n el.addEventListener?.(\"turbo:morph-element\", onMorph)\n if (morphed) return afterMorph()\n wasShell = shellKind(el) !== null\n if (wasShell) arm(false)\n}\n\nexport function disconnect(controller) {\n const wiring = wired.get(controller)\n wired.delete(controller)\n wiring?.off()\n}\n\n// THE materialize entry point: the shell's Stimulus binding (the core's\n// dispatch routes its __materialize action here), the :visible observer, the\n// re-armed event listener and a morph-back all land here, so one in-flight\n// flag makes \"exactly one request\" hold no matter how many of them fire. It\n// rides the ordinary action pipeline (core.proceed → the serialized queue →\n// the request): the reactive:before-dispatch veto, busy markers, error\n// marker/events and the request counter all apply.\n//\n// A `cache:` shell (issue #277) carries its fragment URL: the load is then a\n// GET on the defer pull lane (pending markers, timeout, reactive:error with\n// retry()) instead of the action POST, so the browser's cache can answer —\n// on the first trigger of a later page view, and on every morph-back.\nexport function materialize(controller) {\n return wired.get(controller)?.load()\n}\n\nfunction materializeVetoed(el, core) {\n return core.emit(\"reactive:before-dispatch\", { action: LAZY_MATERIALIZE_ACTION, params: {}, element: el }, { cancelable: true })\n .defaultPrevented\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.\nfunction probe(el) {\n if (!el?.id) return\n if (el.getAttribute?.(\"data-reactive-defer-pending\") !== \"true\") return\n const source = el.getAttribute?.(\"data-reactive-defer-token\") || fragmentSource(el)\n if (source) startFetchDefer(el.id, source)\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "F196EE72B0069C0364756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,242 @@
1
+ // phlex/reactive/features/devtools — development aids: the latency
2
+ // simulator (issue #102), the zero-target diagnostics (issue #237) and the
3
+ // client debug trace (issue #108). One of the feature modules the opt-in
4
+ // client imports on demand (issue #275); in the default client it is part of
5
+ // the one file.
6
+ //
7
+ // When the opt-in client imports it: at registration on a page that carries
8
+ // <meta name="phlex-reactive-env" content="development"> or has a latency
9
+ // delay stored for the tab (effectively eager, in development); at connect
10
+ // for a root stamped data-reactive-debug="true"; and otherwise the first
11
+ // time a verbose root has something to warn about, after which the warning
12
+ // is a tick late. The runtime keeps the gates (the debug and verbose attribute
13
+ // reads) and asks for the module only past them, so a production page with
14
+ // neither never fetches it.
15
+ //
16
+ // This module never imports the runtime: it reaches a controller through the
17
+ // `core` handle.
18
+
19
+ // Latency simulator dev aid (issue #102). On localhost the click→morph round
20
+ // trip is ~5ms, so the pending/loading/optimistic affordances (aria-busy,
21
+ // disable_with, busy_on, optimistic hints) flash by too fast to see while
22
+ // developing or demoing them — the reason LiveView ships enableLatencySim(ms).
23
+ //
24
+ // enableLatencySim(ms) persists the delay to sessionStorage (session-scoped, so
25
+ // it clears when the tab closes — never a config you forget you left on); the
26
+ // controller awaits delay() right before the fetch, stretching the already-set
27
+ // busy window to something visible. disableLatencySim() clears it. NAMED
28
+ // exports (the setConfirmResolver precedent; phlex/reactive/reactive_controller
29
+ // re-exports them) — but importmap module exports are unreachable from the
30
+ // browser console, so attach() ALSO puts them on window.PhlexReactive, and
31
+ // ONLY when the app opts in with
32
+ // <meta name="phlex-reactive-env" content="development">.
33
+ export const LATENCY_KEY = "phlex-reactive:latency"
34
+
35
+ // One-time "sim active" banner guard: delay() warns ONCE while the sim is on,
36
+ // not once per request.
37
+ let latencyBannerShown = false
38
+
39
+ export function enableLatencySim(ms) {
40
+ if (typeof sessionStorage === "undefined") return
41
+ sessionStorage.setItem(LATENCY_KEY, String(ms))
42
+ }
43
+
44
+ export function disableLatencySim() {
45
+ if (typeof sessionStorage === "undefined") return
46
+ sessionStorage.removeItem(LATENCY_KEY)
47
+ // Re-arm the one-time "sim active" banner: turning the sim OFF is the lifecycle
48
+ // boundary, so a later enableLatencySim() in the same session re-announces that
49
+ // the sim is on (otherwise the guard would stay set across an off→on cycle and
50
+ // swallow the banner).
51
+ latencyBannerShown = false
52
+ }
53
+
54
+ // The console handle. importmap module exports aren't reachable from the
55
+ // DevTools console, so the two functions also go on a window global — which
56
+ // the core asks for ONLY when the app authored
57
+ // <meta name="phlex-reactive-env" content="development">. There is NO
58
+ // engine-emitted meta (the engine can't inject into the host layout); the
59
+ // install generator ships the snippet commented. Without the meta: no global
60
+ // handle at all — zero production surface.
61
+ export function attach() {
62
+ if (typeof window !== "undefined") window.PhlexReactive = { enableLatencySim, disableLatencySim }
63
+ }
64
+
65
+ // Forget the one-time active-sim banner (tests).
66
+ export function resetLatencySim() {
67
+ latencyBannerShown = false
68
+ }
69
+
70
+ // If enableLatencySim(ms) stored a delay in sessionStorage, resolve after it —
71
+ // the controller awaits this before the fetch so the busy window (already open
72
+ // since enqueue) is actually visible on localhost. Reads the key LIVE per
73
+ // request — like the CSRF token — so toggling the sim mid-session takes effect
74
+ // on the very next action without a reload. A missing sessionStorage, an absent
75
+ // key, or a non-positive/NaN value resolves immediately (no timer, no delay) —
76
+ // the whole feature is inert for any app that never opts in.
77
+ export function delay() {
78
+ if (typeof sessionStorage === "undefined") return Promise.resolve()
79
+ const ms = Number(sessionStorage.getItem(LATENCY_KEY))
80
+ if (!Number.isFinite(ms) || ms <= 0) return Promise.resolve()
81
+ if (!latencyBannerShown) {
82
+ latencyBannerShown = true
83
+ console.warn(
84
+ `[phlex-reactive] latency simulator ACTIVE — every action is delayed by ${ms}ms. ` +
85
+ "Call PhlexReactive.disableLatencySim() (or clear sessionStorage) to turn it off.",
86
+ )
87
+ }
88
+ return new Promise((resolve) => setTimeout(resolve, ms))
89
+ }
90
+
91
+ // --- The zero-target diagnostics (issue #237) ---------------------------------
92
+ // The runtime calls these only past the verbose gate.
93
+ // --- zero-target diagnostics (issue #237) -----------------------------------
94
+ // An op resolving ZERO targets is indistinguishable from a working no-op, and
95
+ // the documented scoping traps (nested-root ownership filter, root-self
96
+ // selector, stream default scope) all present exactly that way. Under the
97
+ // verbose gate (data-reactive-verbose, stamped when Phlex::Reactive
98
+ // .verbose_errors is on — dev/test by default — or the debug attr) warn ONCE
99
+ // per unique (label, selector, scope) with a targeted hint when the element
100
+ // EXISTS but sits outside the op's scope. Everything below runs only after a
101
+ // zero-match with the gate on; production (no attr) pays one boolean per empty
102
+ // resolution and never probes the DOM.
103
+ //
104
+ // Dedupe is keyed per document (page lifetime): a WeakMap entry per document
105
+ // means a fresh page — or a fresh unit-harness stub — starts clean, and the
106
+ // per-keystroke reducer ($ops) path can never flood the console.
107
+ const zeroTargetWarnSets = new WeakMap()
108
+
109
+ function zeroTargetAlreadyWarned(key) {
110
+ const doc = globalThis.document
111
+ if (!doc) return true
112
+ let seen = zeroTargetWarnSets.get(doc)
113
+ if (!seen) {
114
+ seen = new Set()
115
+ zeroTargetWarnSets.set(doc, seen)
116
+ }
117
+ if (seen.has(key)) return true
118
+ seen.add(key)
119
+ return false
120
+ }
121
+
122
+ // Guarded probes: unit harnesses stub partial documents/roots, and an exotic
123
+ // selector could throw — a diagnostic must never break the op pipeline.
124
+ function countMatches(node, selector) {
125
+ try {
126
+ return node?.querySelectorAll?.(selector)?.length ?? 0
127
+ } catch {
128
+ return 0
129
+ }
130
+ }
131
+
132
+ function emitZeroTargetWarn(label, to, scope, hint) {
133
+ if (zeroTargetAlreadyWarned(`${label}|${to}|${scope}`)) return
134
+ console.warn(`[phlex-reactive] ${label} matched zero targets for selector "${to}" (${scope})${hint}`)
135
+ }
136
+
137
+ // The stream-path diagnoser (reactive:js): no root, or the stream's target
138
+ // root. No ownership filter exists here, so the only trap is the target-root
139
+ // scope: the selector matches document-wide but the op was scoped to the
140
+ // stream's target root.
141
+ export function diagnoseStream(name, args, root) {
142
+ const to = args.to
143
+ if (typeof to !== "string" || to === "" || to === "@root") return
144
+ let hint = ""
145
+ if (root && !args.global) {
146
+ const n = countMatches(globalThis.document, to)
147
+ if (n > 0) hint = ` — it matches ${n} element(s) outside the stream's target root; use global: true`
148
+ }
149
+ emitZeroTargetWarn(`client op "${name}"`, to, root ? `scoped to #${root.id || "?"}` : "document-scoped", hint)
150
+ }
151
+
152
+ // The runtime's call-ins below take the controller; each reads its root.
153
+
154
+ export function missingRoot(targetId) {
155
+ if (!zeroTargetAlreadyWarned(`missing-root|#${targetId}`)) {
156
+ console.warn(`[phlex-reactive] reactive:js stream target root #${targetId} is not in the DOM — its ops were dropped`)
157
+ }
158
+ }
159
+
160
+ // Issue #237: called when a selector-form target resolved to ZERO elements on
161
+ // this root. Gated + deduped (module helpers); builds the trap-specific hint:
162
+ // the root-self selector (root-scoped resolution never includes the root),
163
+ // the nested-reactive-root ownership filter, or plain out-of-scope. All DOM
164
+ // probes run only here — after a zero-match with the gate on.
165
+ export function diagnose(controller, label, args) {
166
+ const root = controller.element
167
+ const to = args.to
168
+ if (typeof to !== "string" || to === "" || to === "@root") return
169
+ let hint = ""
170
+ if (!args.global) {
171
+ if (root?.matches?.(to)) {
172
+ hint = " — the selector matches this component's own root, which root-scoped resolution never includes; use to: :root"
173
+ } else if (countMatches(root, to) > 0) {
174
+ hint = " — it matches only inside a nested reactive root (excluded by ownership scoping); use global: true"
175
+ } else {
176
+ const n = countMatches(globalThis.document, to)
177
+ if (n > 0) hint = ` — it matches ${n} element(s) outside this scope; use global: true`
178
+ }
179
+ }
180
+ emitZeroTargetWarn(label, to, `scoped to #${root?.id || "?"}`, hint)
181
+ }
182
+
183
+ // Issue #271: runOps fired but no record matched the event — a hand-edited
184
+ // attr or a descriptor the matcher doesn't know. Verbose gate only, deduped.
185
+ export function noBinding(controller, event) {
186
+ const id = controller.element?.id || "?"
187
+ if (zeroTargetAlreadyWarned(`no-binding|${event.type}|${id}`)) return
188
+ console.warn(`[phlex-reactive] runOps on #${id} found no on_client binding matching a "${event.type}" event — nothing ran`)
189
+ }
190
+
191
+ // --- The debug trace (issue #108) -----------------------------------------------
192
+
193
+ // Parse a turbo-stream response's action + target pairs for the debug trace,
194
+ // from the body text #perform ALREADY read (never a re-fetch). NAMES only — the
195
+ // <template> contents (rendered HTML, the fresh token) are deliberately not
196
+ // touched. A non-turbo-stream / empty body yields [] (nothing to report).
197
+ function debugStreams(body) {
198
+ if (!body) return []
199
+ const streams = []
200
+ const re = /<turbo-stream\b([^>]*)>/g
201
+ let match
202
+ while ((match = re.exec(body)) !== null) {
203
+ const attrs = match[1]
204
+ const action = attrs.match(/\baction="([^"]*)"/)?.[1] ?? "?"
205
+ const target = attrs.match(/\btarget="([^"]*)"/)?.[1]
206
+ streams.push(target ? `${action} → #${target}` : action)
207
+ }
208
+ return streams
209
+ }
210
+
211
+ // console.group ONE dispatch (issue #108). Carries NAMES + outcomes ONLY — the
212
+ // signed token VALUE and every field/param VALUE are deliberately absent (they
213
+ // may be sensitive; the whole point is observability without leaking data). The
214
+ // caller passes the info it already holds so nothing is recomputed or re-fetched:
215
+ // { action, paramNames, fieldNames, encoding, status, streams, tokenRefreshed, ms }
216
+ // `console.groupCollapsed` keeps the console tidy (one collapsed line per action).
217
+ export function trace(controller, info) {
218
+ const { action, status, ms } = info
219
+ // The client can't name the component CLASS (it's inside the signed, opaque
220
+ // token — never decoded here), but the root's id is the stable client-side
221
+ // handle (e.g. #todo_42), so the header reads `reactive #todo_42 rename → …`.
222
+ const id = controller.element?.id
223
+ const who = id ? `#${id} ` : ""
224
+ const header = `reactive ${who}${action} → ${status ?? "—"} (${Math.round(ms)}ms)`
225
+ /* eslint-disable no-console */
226
+ console.groupCollapsed(header)
227
+ console.log(`params: [${info.paramNames.join(", ")}] + collected: [${info.fieldNames.join(", ")}]`)
228
+ console.log(`encoding: ${info.encoding}`)
229
+ if (info.streams.length) console.log(`streams: ${info.streams.join(" ")}`)
230
+ console.log(`token: ${info.tokenRefreshed ? "refreshed ✓" : "unchanged"}`)
231
+ console.groupEnd()
232
+ /* eslint-enable no-console */
233
+ }
234
+
235
+ // Debug (issue #108): fold the response body #perform already read into the
236
+ // trace — the stream action/target pairs and whether a token refresh arrived
237
+ // (a boolean; the token VALUE is intentionally not stored). Shared by the
238
+ // success and the non-OK-turbo-stream branches so both log the same shape.
239
+ export function recordBody(debug, body, freshToken) {
240
+ debug.streams = debugStreams(body)
241
+ debug.tokenRefreshed = freshToken != null
242
+ }
@@ -0,0 +1,4 @@
1
+ var p="phlex-reactive:latency",c=!1;function f(e){if(typeof sessionStorage>"u")return;sessionStorage.setItem("phlex-reactive:latency",String(e))}function h(){if(typeof sessionStorage>"u")return;sessionStorage.removeItem("phlex-reactive:latency"),c=!1}function y(){if(typeof window<"u")window.PhlexReactive={enableLatencySim:f,disableLatencySim:h}}function x(){c=!1}function b(){if(typeof sessionStorage>"u")return Promise.resolve();let e=Number(sessionStorage.getItem("phlex-reactive:latency"));if(!Number.isFinite(e)||e<=0)return Promise.resolve();if(!c)c=!0,console.warn(`[phlex-reactive] latency simulator ACTIVE — every action is delayed by ${e}ms. Call PhlexReactive.disableLatencySim() (or clear sessionStorage) to turn it off.`);return new Promise((t)=>setTimeout(t,e))}var u=new WeakMap;function l(e){let t=globalThis.document;if(!t)return!0;let o=u.get(t);if(!o)o=new Set,u.set(t,o);if(o.has(e))return!0;return o.add(e),!1}function a(e,t){try{return e?.querySelectorAll?.(t)?.length??0}catch{return 0}}function d(e,t,o,n){if(l(`${e}|${t}|${o}`))return;console.warn(`[phlex-reactive] ${e} matched zero targets for selector "${t}" (${o})${n}`)}function w(e,t,o){let n=t.to;if(typeof n!=="string"||n===""||n==="@root")return;let r="";if(o&&!t.global){let s=a(globalThis.document,n);if(s>0)r=` — it matches ${s} element(s) outside the stream's target root; use global: true`}d(`client op "${e}"`,n,o?`scoped to #${o.id||"?"}`:"document-scoped",r)}function S(e){if(!l(`missing-root|#${e}`))console.warn(`[phlex-reactive] reactive:js stream target root #${e} is not in the DOM — its ops were dropped`)}function v(e,t,o){let n=e.element,r=o.to;if(typeof r!=="string"||r===""||r==="@root")return;let s="";if(!o.global)if(n?.matches?.(r))s=" — the selector matches this component's own root, which root-scoped resolution never includes; use to: :root";else if(a(n,r)>0)s=" — it matches only inside a nested reactive root (excluded by ownership scoping); use global: true";else{let i=a(globalThis.document,r);if(i>0)s=` — it matches ${i} element(s) outside this scope; use global: true`}d(t,r,`scoped to #${n?.id||"?"}`,s)}function T(e,t){let o=e.element?.id||"?";if(l(`no-binding|${t.type}|${o}`))return;console.warn(`[phlex-reactive] runOps on #${o} found no on_client binding matching a "${t.type}" event — nothing ran`)}function g(e){if(!e)return[];let t=[],o=/<turbo-stream\b([^>]*)>/g,n;while((n=o.exec(e))!==null){let r=n[1],s=r.match(/\baction="([^"]*)"/)?.[1]??"?",i=r.match(/\btarget="([^"]*)"/)?.[1];t.push(i?`${s} → #${i}`:s)}return t}function E(e,t){let{action:o,status:n,ms:r}=t,s=e.element?.id,m=`reactive ${s?`#${s} `:""}${o} → ${n??"—"} (${Math.round(r)}ms)`;if(console.groupCollapsed(m),console.log(`params: [${t.paramNames.join(", ")}] + collected: [${t.fieldNames.join(", ")}]`),console.log(`encoding: ${t.encoding}`),t.streams.length)console.log(`streams: ${t.streams.join(" ")}`);console.log(`token: ${t.tokenRefreshed?"refreshed ✓":"unchanged"}`),console.groupEnd()}function L(e,t,o){e.streams=g(t),e.tokenRefreshed=o!=null}export{p as LATENCY_KEY,y as attach,b as delay,v as diagnose,w as diagnoseStream,h as disableLatencySim,f as enableLatencySim,S as missingRoot,T as noBinding,L as recordBody,x as resetLatencySim,E as trace};
2
+
3
+ //# debugId=FE47EA9129EA3AE764756E2164756E21
4
+ //# sourceMappingURL=devtools.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["features/devtools.js"],
4
+ "sourcesContent": [
5
+ "// phlex/reactive/features/devtools — development aids: the latency\n// simulator (issue #102), the zero-target diagnostics (issue #237) and the\n// client debug trace (issue #108). One of the feature modules the opt-in\n// client imports on demand (issue #275); in the default client it is part of\n// the one file.\n//\n// When the opt-in client imports it: at registration on a page that carries\n// <meta name=\"phlex-reactive-env\" content=\"development\"> or has a latency\n// delay stored for the tab (effectively eager, in development); at connect\n// for a root stamped data-reactive-debug=\"true\"; and otherwise the first\n// time a verbose root has something to warn about, after which the warning\n// is a tick late. The runtime keeps the gates (the debug and verbose attribute\n// reads) and asks for the module only past them, so a production page with\n// neither never fetches it.\n//\n// This module never imports the runtime: it reaches a controller through the\n// `core` handle.\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); the\n// controller awaits delay() right before the fetch, stretching the already-set\n// busy window to something visible. disableLatencySim() clears it. NAMED\n// exports (the setConfirmResolver precedent; phlex/reactive/reactive_controller\n// re-exports them) — but importmap module exports are unreachable from the\n// browser console, so attach() ALSO puts them on window.PhlexReactive, and\n// ONLY when the app opts in with\n// <meta name=\"phlex-reactive-env\" content=\"development\">.\nexport const LATENCY_KEY = \"phlex-reactive:latency\"\n\n// One-time \"sim active\" banner guard: delay() warns ONCE while the sim is on,\n// 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).\n latencyBannerShown = false\n}\n\n// The console handle. importmap module exports aren't reachable from the\n// DevTools console, so the two functions also go on a window global — which\n// the core asks for ONLY when the app authored\n// <meta name=\"phlex-reactive-env\" content=\"development\">. There is NO\n// 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 — zero production surface.\nexport function attach() {\n if (typeof window !== \"undefined\") window.PhlexReactive = { enableLatencySim, disableLatencySim }\n}\n\n// Forget the one-time active-sim banner (tests).\nexport function resetLatencySim() {\n latencyBannerShown = false\n}\n\n// If enableLatencySim(ms) stored a delay in sessionStorage, resolve after it —\n// the controller awaits this 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.\nexport function delay() {\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// --- The zero-target diagnostics (issue #237) ---------------------------------\n// The runtime calls these only past the verbose gate.\n// --- zero-target diagnostics (issue #237) -----------------------------------\n// An op resolving ZERO targets is indistinguishable from a working no-op, and\n// the documented scoping traps (nested-root ownership filter, root-self\n// selector, stream default scope) all present exactly that way. Under the\n// verbose gate (data-reactive-verbose, stamped when Phlex::Reactive\n// .verbose_errors is on — dev/test by default — or the debug attr) warn ONCE\n// per unique (label, selector, scope) with a targeted hint when the element\n// EXISTS but sits outside the op's scope. Everything below runs only after a\n// zero-match with the gate on; production (no attr) pays one boolean per empty\n// resolution and never probes the DOM.\n//\n// Dedupe is keyed per document (page lifetime): a WeakMap entry per document\n// means a fresh page — or a fresh unit-harness stub — starts clean, and the\n// per-keystroke reducer ($ops) path can never flood the console.\nconst zeroTargetWarnSets = new WeakMap()\n\nfunction zeroTargetAlreadyWarned(key) {\n const doc = globalThis.document\n if (!doc) return true\n let seen = zeroTargetWarnSets.get(doc)\n if (!seen) {\n seen = new Set()\n zeroTargetWarnSets.set(doc, seen)\n }\n if (seen.has(key)) return true\n seen.add(key)\n return false\n}\n\n// Guarded probes: unit harnesses stub partial documents/roots, and an exotic\n// selector could throw — a diagnostic must never break the op pipeline.\nfunction countMatches(node, selector) {\n try {\n return node?.querySelectorAll?.(selector)?.length ?? 0\n } catch {\n return 0\n }\n}\n\nfunction emitZeroTargetWarn(label, to, scope, hint) {\n if (zeroTargetAlreadyWarned(`${label}|${to}|${scope}`)) return\n console.warn(`[phlex-reactive] ${label} matched zero targets for selector \"${to}\" (${scope})${hint}`)\n}\n\n// The stream-path diagnoser (reactive:js): no root, or the stream's target\n// root. No ownership filter exists here, so the only trap is the target-root\n// scope: the selector matches document-wide but the op was scoped to the\n// stream's target root.\nexport function diagnoseStream(name, args, root) {\n const to = args.to\n if (typeof to !== \"string\" || to === \"\" || to === \"@root\") return\n let hint = \"\"\n if (root && !args.global) {\n const n = countMatches(globalThis.document, to)\n if (n > 0) hint = ` — it matches ${n} element(s) outside the stream's target root; use global: true`\n }\n emitZeroTargetWarn(`client op \"${name}\"`, to, root ? `scoped to #${root.id || \"?\"}` : \"document-scoped\", hint)\n}\n\n// The runtime's call-ins below take the controller; each reads its root.\n\nexport function missingRoot(targetId) {\n if (!zeroTargetAlreadyWarned(`missing-root|#${targetId}`)) {\n console.warn(`[phlex-reactive] reactive:js stream target root #${targetId} is not in the DOM — its ops were dropped`)\n }\n}\n\n// Issue #237: called when a selector-form target resolved to ZERO elements on\n// this root. Gated + deduped (module helpers); builds the trap-specific hint:\n// the root-self selector (root-scoped resolution never includes the root),\n// the nested-reactive-root ownership filter, or plain out-of-scope. All DOM\n// probes run only here — after a zero-match with the gate on.\nexport function diagnose(controller, label, args) {\n const root = controller.element\n const to = args.to\n if (typeof to !== \"string\" || to === \"\" || to === \"@root\") return\n let hint = \"\"\n if (!args.global) {\n if (root?.matches?.(to)) {\n hint = \" — the selector matches this component's own root, which root-scoped resolution never includes; use to: :root\"\n } else if (countMatches(root, to) > 0) {\n hint = \" — it matches only inside a nested reactive root (excluded by ownership scoping); use global: true\"\n } else {\n const n = countMatches(globalThis.document, to)\n if (n > 0) hint = ` — it matches ${n} element(s) outside this scope; use global: true`\n }\n }\n emitZeroTargetWarn(label, to, `scoped to #${root?.id || \"?\"}`, hint)\n}\n\n// Issue #271: runOps fired but no record matched the event — a hand-edited\n// attr or a descriptor the matcher doesn't know. Verbose gate only, deduped.\nexport function noBinding(controller, event) {\n const id = controller.element?.id || \"?\"\n if (zeroTargetAlreadyWarned(`no-binding|${event.type}|${id}`)) return\n console.warn(`[phlex-reactive] runOps on #${id} found no on_client binding matching a \"${event.type}\" event — nothing ran`)\n}\n\n// --- The debug trace (issue #108) -----------------------------------------------\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).\nfunction 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).\nexport function trace(controller, 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 id = controller.element?.id\n const who = id ? `#${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// 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.\nexport function recordBody(debug, body, freshToken) {\n debug.streams = debugStreams(body)\n debug.tokenRefreshed = freshToken != null\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "FE47EA9129EA3AE764756E2164756E21",
9
+ "names": []
10
+ }