phlex-reactive 0.16.7 → 0.16.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -1
- data/README.md +106 -10
- data/app/javascript/phlex/reactive/core.min.js +6 -6
- data/app/javascript/phlex/reactive/core.min.js.map +3 -3
- data/app/javascript/phlex/reactive/early.js +28 -1
- data/app/javascript/phlex/reactive/early.min.js +2 -2
- data/app/javascript/phlex/reactive/early.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/bindings.js +52 -17
- data/app/javascript/phlex/reactive/features/bindings.min.js +2 -2
- data/app/javascript/phlex/reactive/features/bindings.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/compute.js +32 -4
- data/app/javascript/phlex/reactive/features/compute.min.js +2 -2
- data/app/javascript/phlex/reactive/features/compute.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/devtools.js +15 -5
- data/app/javascript/phlex/reactive/features/devtools.min.js +2 -2
- data/app/javascript/phlex/reactive/features/devtools.min.js.map +3 -3
- data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -4
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +6 -6
- data/app/javascript/phlex/reactive/runtime.js +146 -22
- data/lib/phlex/reactive/component/dsl.rb +2 -0
- data/lib/phlex/reactive/component/helpers.rb +50 -4
- data/lib/phlex/reactive/js.rb +211 -41
- data/lib/phlex/reactive/response.rb +4 -0
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +1 -1
|
@@ -26,6 +26,8 @@ import { computeReducer } from "phlex/reactive/compute"
|
|
|
26
26
|
// (and ONLY one of ours)
|
|
27
27
|
// opsSig the reducer's last $ops chain (issue #226), the rising-edge latch
|
|
28
28
|
// seed the connect-time seed, when the root opts in
|
|
29
|
+
// nested re-seeds when a nested root holding a shared field connects
|
|
30
|
+
// share the input listener for shared fields in nested roots (issue #440)
|
|
29
31
|
const contexts = new WeakMap()
|
|
30
32
|
|
|
31
33
|
function ctx(controller, core) {
|
|
@@ -58,7 +60,22 @@ export function connect(controller, core, _morphed, pending) {
|
|
|
58
60
|
if (seeds) {
|
|
59
61
|
c.seed = () => recompute(c)
|
|
60
62
|
c.root.addEventListener?.("turbo:morph-element", c.seed)
|
|
63
|
+
// Issue #440: a nested root that comes back through a plain replace (a
|
|
64
|
+
// morph bubbles turbo:morph-element above), or restored a draft without an
|
|
65
|
+
// input event, announces itself with a bubbling event; when it holds a
|
|
66
|
+
// shared field this root reads, its new value is one of this root's inputs.
|
|
67
|
+
c.nested = (event) =>
|
|
68
|
+
event.target !== c.root && [...event.target.querySelectorAll(SHARED_FIELD)].some(c.core.bindingOwns) && c.seed()
|
|
69
|
+
for (const type of NESTED_EVENTS) c.root.addEventListener?.(type, c.nested)
|
|
61
70
|
}
|
|
71
|
+
// Issue #440: Stimulus never routes an event from inside a nested reactive
|
|
72
|
+
// root to this root's input->reactive#recompute, yet a control there marked
|
|
73
|
+
// data-reactive-shared-field is one of this root's inputs. Its edits come
|
|
74
|
+
// through this listener instead; an owned field's stay Stimulus's (a second
|
|
75
|
+
// run would be a double one); any other edit costs one attribute read.
|
|
76
|
+
c.share = (event) =>
|
|
77
|
+
event.target?.matches?.(SHARED_FIELD) && !c.core.owns(event.target) && c.core.bindingOwns(event.target) && recompute(c, event)
|
|
78
|
+
c.root.addEventListener?.("input", c.share)
|
|
62
79
|
// The one pass a root that waited for this module owes: its seed, or the
|
|
63
80
|
// recompute an edit in the window asked for.
|
|
64
81
|
if (seeds || pending?.edited) recompute(c)
|
|
@@ -69,6 +86,8 @@ export function disconnect(controller) {
|
|
|
69
86
|
if (!c) return
|
|
70
87
|
contexts.delete(controller)
|
|
71
88
|
if (c.seed) c.root.removeEventListener?.("turbo:morph-element", c.seed)
|
|
89
|
+
for (const type of NESTED_EVENTS) c.root.removeEventListener?.(type, c.nested)
|
|
90
|
+
c.root.removeEventListener?.("input", c.share)
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
// Re-run the seed (for a feature that changed field values after connect:
|
|
@@ -84,6 +103,11 @@ function publicRecompute(controller, core, event) {
|
|
|
84
103
|
}
|
|
85
104
|
export { publicRecompute as recompute }
|
|
86
105
|
|
|
106
|
+
// The marker reactive_shared_field renders (issue #434), and the bubbling
|
|
107
|
+
// events a nested root announces fresh field values with (issue #440).
|
|
108
|
+
const SHARED_FIELD = "[data-reactive-shared-field]"
|
|
109
|
+
const NESTED_EVENTS = ["reactive:connect", "reactive:persist-restored"]
|
|
110
|
+
|
|
87
111
|
// The control for a name nothing owned resolves to.
|
|
88
112
|
const COMPUTE_NO_CONTROL = Object.freeze({ el: null, kind: "" })
|
|
89
113
|
|
|
@@ -252,8 +276,9 @@ function parseComputeInputs(c) {
|
|
|
252
276
|
// The declared compute input the event just edited — the reducer's
|
|
253
277
|
// meta.changed (issue #75). The triggering field counts only when it is a
|
|
254
278
|
// named form control OWNED by this root (not a nested reactive root's, issue
|
|
255
|
-
// #15
|
|
256
|
-
// (a direct call, an
|
|
279
|
+
// #15, unless it is a shared field just below, issue #440) AND its name is
|
|
280
|
+
// among the declared compute inputs; anything else (a direct call, an
|
|
281
|
+
// unowned/undeclared target) yields null.
|
|
257
282
|
//
|
|
258
283
|
// Scope-aware (issue #184): under data-reactive-scope, the edited field's DOM
|
|
259
284
|
// name is scoped (order[allowance]) while the declared inputs are BARE
|
|
@@ -265,7 +290,7 @@ function changedComputeField(c, event, inputs, scope) {
|
|
|
265
290
|
if (!target?.name || typeof target.closest !== "function") return null
|
|
266
291
|
const bare = unscopeName(target.name, scope)
|
|
267
292
|
if (!inputs.includes(bare)) return null
|
|
268
|
-
return c.core.
|
|
293
|
+
return c.core.bindingOwns(target) ? bare : null
|
|
269
294
|
}
|
|
270
295
|
|
|
271
296
|
// Strip a leading `scope[…]` wrapper off a DOM field name, returning the bare
|
|
@@ -414,7 +439,10 @@ function recompute(c, event) {
|
|
|
414
439
|
const scope = c.root.getAttribute?.("data-reactive-scope") || null
|
|
415
440
|
const scoped = (name) => (scope && !name.includes("[") ? `${scope}[${name}]` : name)
|
|
416
441
|
|
|
417
|
-
|
|
442
|
+
// Inputs AND outputs resolve through what the bindings read (issue #440):
|
|
443
|
+
// a control marked data-reactive-shared-field just below is this root's
|
|
444
|
+
// too. Text nodes (reactive_text) stay strictly this root's own.
|
|
445
|
+
const owns = c.core.bindingOwnership()
|
|
418
446
|
const byName = new Map()
|
|
419
447
|
const ownedControl = (name) => {
|
|
420
448
|
const known = byName.get(name)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{computeReducer as
|
|
1
|
+
import{computeReducer as _}from"phlex/reactive/compute";var b=new WeakMap;function C(e,t){let n=b.get(e);if(!n)b.set(e,n={root:e.element,core:t,ours:new WeakSet,opsSig:null});return n}function K(e,t,n,o){let r=C(e,t),c=Z(r);if(c){r.seed=()=>A(r),r.root.addEventListener?.("turbo:morph-element",r.seed),r.nested=(s)=>s.target!==r.root&&[...s.target.querySelectorAll(v)].some(r.core.bindingOwns)&&r.seed();for(let s of N)r.root.addEventListener?.(s,r.nested)}if(r.share=(s)=>s.target?.matches?.(v)&&!r.core.owns(s.target)&&r.core.bindingOwns(s.target)&&A(r,s),r.root.addEventListener?.("input",r.share),c||o?.edited)A(r)}function Q(e){let t=b.get(e);if(!t)return;if(b.delete(e),t.seed)t.root.removeEventListener?.("turbo:morph-element",t.seed);for(let n of N)t.root.removeEventListener?.(n,t.nested);t.root.removeEventListener?.("input",t.share)}function X(e){b.get(e)?.seed?.()}function Y(e,t,n){return A(C(e,t),n)}var v="[data-reactive-shared-field]",N=["reactive:connect","reactive:persist-restored"],M=Object.freeze({el:null,kind:""}),j=/^#[A-Za-z_][\w-]*$/;function y({el:e,kind:t}){if(!e)return"";if(t==="checkbox")return e.checked?"true":"false";if(t==="radio")return e.checked?e.value??"":"";return e.value??""}function L(e){if(typeof e==="boolean")return e;if(e==null)return!1;let t=String(e);return t!==""&&t!=="0"&&t!=="false"}function q(e,t){if(t==="string")return y(e);let n=e.kind==="checkbox";if(t==="boolean")return n?Boolean(e.el.checked):L(y(e));if(n)return e.el.checked?1:0;let o=Number(y(e));return Number.isFinite(o)?o:0}function J(e,t,{el:n,kind:o},r,c){if(o==="checkbox"){let s=L(c);if(Boolean(n.checked)===s)return null;return n.checked=s,n}if(o==="radio")return W(e,t,r,String(c));if(String(c)===n.value)return null;return n.value=c,n}function W(e,t,n,o){let r=null;for(let c of e.querySelectorAll(`[name="${n}"]`)){if(c.type!=="radio"||!t(c))continue;let s=c.value===o;if(Boolean(c.checked)===s)continue;if(c.checked=s,s)r=c;else r??=c}return r}function B(e){if(e==null)return null;let t=Array.isArray(e)?e:e.ops;if(Array.isArray(t))return t.length>0?t:null;return console.warn("[phlex-reactive] $ops must be an ops chain or a [[op, args], ...] list — skipped"),null}function D(e){if(typeof e==="string"&&j.test(e))return!0;return console.warn(`[phlex-reactive] refused cross-root mirror target ${JSON.stringify(e)} — skipped`),!1}function F(e,t,n){let o=t===null?null:JSON.stringify(t),r=o!==null&&o!==e.opsSig&&n;if(e.opsSig=o,!r)return;e.core.applyOps(t,"@root")}function I(e,t){let n=e.root.getAttribute(t);if(!n)return[];try{let o=JSON.parse(n);return Array.isArray(o)?o:[]}catch{return[]}}function z(e){let t=e.root.getAttribute("data-reactive-compute-inputs-param");if(!t)return[];try{let n=JSON.parse(t);if(Array.isArray(n))return n.map((o)=>[o,"number"]);if(n&&typeof n==="object")return Object.entries(n);return[]}catch{return[]}}function P(e,t,n,o){let r=t?.target;if(!r?.name||typeof r.closest!=="function")return null;let c=V(r.name,o);if(!n.includes(c))return null;return e.core.bindingOwns(r)?c:null}function V(e,t){if(!t)return e;let n=`${t}[`;return e.startsWith(n)&&e.endsWith("]")?e.slice(n.length,-1):e}function E(e,t,n){let o=String(n);for(let r of H(e,t)){if(r.textContent===o)continue;r.textContent=o}}function H(e,t){let n=e.root.querySelectorAll(`[data-reactive-text="${t}"]`);return Array.from(n).filter((o)=>e.core.owns(o))}function w(e,t,n){let o=U(e);for(let[r,c]of Object.entries(o)){let s=r in t?null:n(r),f=r in t?t[r]:s.el?y(s):void 0;if(f===void 0||f===null)continue;let a=String(f);for(let g of Array.isArray(c)?c:[c]){if(!D(g))continue;for(let h of document.querySelectorAll(g)){if(h.textContent===a)continue;h.textContent=a}}}}function U(e){let t=e.root.getAttribute("data-reactive-compute-mirror-param");if(!t)return{};try{let n=JSON.parse(t);return n&&typeof n==="object"&&!Array.isArray(n)?n:{}}catch{return{}}}function Z(e){return e.root.getAttribute?.("data-reactive-compute-seed")==="true"}function A(e,t){if(t&&e.ours.has(t))return;let n=z(e),o=n.map(([i])=>i),r=e.root.getAttribute?.("data-reactive-scope")||null,c=(i)=>r&&!i.includes("[")?`${r}[${i}]`:i,s=e.core.bindingOwnership(),f=new Map,a=(i)=>{let u=f.get(i);if(u)return u;let p=null,x=null,d=null;for(let m of e.root.querySelectorAll(`[name="${c(i)}"]`)){if(!s(m))continue;let k=m.type;if(k==="checkbox"||k==="radio"&&m.checked){d={el:m,kind:k};break}if(k==="radio")p??=m;else x??=m}if(!d)if(p)d={el:p,kind:"radio"};else d=x?{el:x,kind:""}:M;return f.set(i,d),d};for(let i of o)E(e,i,y(a(i)));let g=e.root.getAttribute("data-reactive-compute-reducer-param"),h=g?_(g):null;if(!h){w(e,{},a);return}let R=I(e,"data-reactive-compute-outputs-param"),S={};for(let[i,u]of n)S[i]=q(a(i),u);let l=h(S,{changed:P(e,t,o,r)})||{},T=B(l.$ops),O=[];for(let i of R){if(i==="$ops"||!(i in l))continue;let u=a(i);if(!u.el)continue;let p=J(e.root,s,u,c(i),l[i]);if(p)O.push(p)}for(let i of Object.keys(l)){if(i==="$ops")continue;let u=l[i];if(u===void 0||u===null)continue;E(e,i,u)}w(e,l,a);for(let i of O){let u=new Event("input",{bubbles:!0});e.ours.add(u),i.dispatchEvent(u)}F(e,T,Boolean(t))}export{K as connect,Q as disconnect,Y as recompute,X as seed};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=5547A787C6231C5664756E2164756E21
|
|
4
4
|
//# sourceMappingURL=compute.min.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["features/compute.js"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// phlex/reactive/features/compute — client-side computes (data binding,\n// issues #104, #159, #183, #199, #262): `recompute`, the connect-time seed,\n// text mirrors and the reducer's $ops. 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// The reducer registry is the compute seam (phlex/reactive/compute), imported\n// here by its bare name — so the runtime no longer imports it, and a page\n// with no compute binding never resolves it. The seed writes output fields a\n// request collects, so with the opt-in client the feature `gates`: a root's\n// requests wait for it. While the module is on its way, the opt-in entry\n// records that the user edited a field (`pending.edited`), and connect() runs\n// ONE recompute then: the reducer reads the inputs as the user left them, so\n// nothing they typed is overwritten, and a `recompute` the window dispatched\n// has run exactly once when it is over.\n//\n// This module never imports the runtime: it reaches a controller through the\n// `core` handle.\nimport { computeReducer } from \"phlex/reactive/compute\"\n\n// controller -> its context: the root, the core handle, and what used to be\n// the controller's own fields for this feature. Short field names: the\n// minifier never renames a property (issue #310).\n// ours the `input` events recompute dispatched for its OWN output\n// writes (issue #183), so re-entering from one skips the reducer\n// (and ONLY one of ours)\n// opsSig the reducer's last $ops chain (issue #226), the rising-edge latch\n// seed the connect-time seed, when the root opts in\nconst contexts = new WeakMap()\n\nfunction ctx(controller, core) {\n let c = contexts.get(controller)\n if (!c) contexts.set(controller, (c = { root: controller.element, core, ours: new WeakSet(), opsSig: null }))\n return c\n}\n\nexport function connect(controller, core, _morphed, pending) {\n const c = ctx(controller, core)\n // Connect-time compute seed (issue #199) — ONLY when the root carries a\n // reactive_compute binding that opts in (data-reactive-compute-seed). A\n // freshly-rendered compute root (a first paint, or a server validation-error\n // re-render that replaced the body) computed NOTHING until the first user\n // `input`; apps worked around it by dispatching a synthetic seed `input` on\n // connect, but the compute root is a distinct Stimulus controller that may\n // connect a frame later, so the seed raced its own wiring. Running ONE\n // recompute() HERE — after Stimulus has fully connected the controller and\n // wired the input->recompute delegation — runs the whole single-pass write\n // set (issue #183) synchronously, so every declared output, text sink, and\n // cross-root mirror paints from one reducer result. It is client-only\n // (recompute never enqueues a round trip) and idempotent (change-guarded\n // writes make a re-seed a no-op). A plain replace re-connects and re-seeds;\n // an in-place morph keeps the element CONNECTED and fires no Stimulus\n // lifecycle, so ALSO re-seed on turbo:morph-element. No event is passed, so\n // meta.changed is null — the correct \"no field edited yet\" seed semantics; a\n // convergent reducer's default branch computes the full settled set (see\n // compute.js CONVERGENCE REQUIREMENT).\n const seeds = computeSeedEnabled(c)\n if (seeds) {\n c.seed = () => recompute(c)\n c.root.addEventListener?.(\"turbo:morph-element\", c.seed)\n }\n // The one pass a root that waited for this module owes: its seed, or the\n // recompute an edit in the window asked for.\n if (seeds || pending?.edited) recompute(c)\n}\n\nexport function disconnect(controller) {\n const c = contexts.get(controller)\n if (!c) return\n contexts.delete(controller)\n if (c.seed) c.root.removeEventListener?.(\"turbo:morph-element\", c.seed)\n}\n\n// Re-run the seed (for a feature that changed field values after connect:\n// the draft restore).\nexport function seed(controller) {\n contexts.get(controller)?.seed?.()\n}\n\n// The Stimulus action (input->reactive#recompute); the controller's method of\n// the same name hands the event here.\nfunction publicRecompute(controller, core, event) {\n return recompute(ctx(controller, core), event)\n}\nexport { publicRecompute as recompute }\n\n// The control for a name nothing owned resolves to.\nconst COMPUTE_NO_CONTROL = Object.freeze({ el: null, kind: \"\" })\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-]*$/\n\n// The TEXT reading of a compute control (issue #262) — what a :string input\n// hands the reducer and what the identity/cross-root mirrors paint. A checkbox\n// reads its CHECKED STATE (\"true\"/\"false\", the strings reactive_show compares\n// against): its .value is a constant — \"1\", \"on\", whatever the markup says —\n// so reading it told the reducer nothing. A radio reads its value only while\n// checked (\"\" otherwise; the resolver hands over the checked radio of a group).\n// Anything else reads .value, as it always has.\n//\n// Every compute helper takes a CONTROL — the { el, kind } record #recompute's\n// resolver builds, kind being \"checkbox\", \"radio\" or \"\" — and never re-reads\n// el.type: the resolver reads it ONCE per name. Re-reading it in each helper\n// cost the 30-input calculator bench ~60% (16.8 → 27 µs/iter, measured).\nfunction computeText({ el, kind }) {\n if (!el) return \"\"\n if (kind === \"checkbox\") return el.checked ? \"true\" : \"false\"\n if (kind === \"radio\") return el.checked ? (el.value ?? \"\") : \"\"\n return el.value ?? \"\"\n}\n\n// Whether a value counts as \"on\" — for a :boolean input read off a control that\n// is not a checkbox, and for an output written INTO a checkbox. A boolean is\n// itself; otherwise \"\", \"0\" and \"false\" are off (what a hidden flag field or a\n// reducer returning 0 means) and anything else is on.\nfunction computeTruthy(value) {\n if (typeof value === \"boolean\") return value\n if (value == null) return false\n const text = String(value)\n return text !== \"\" && text !== \"0\" && text !== \"false\"\n}\n\n// One declared input's value for the reducer, coerced by its declared type\n// (issue #104; checked-state controls issue #262):\n//\n// \"string\" → the text reading, raw (blank/absent → \"\")\n// \"boolean\" → a checkbox's checked state; any other control by computeTruthy\n// \"number\" → a checkbox is 1/0; anything else through Number (blank/NaN → 0,\n// the nanToZero the hand-written calculators use)\n//\n// A checkbox is 1/0 and never Number(its value): a box is a yes/no, and a\n// reducer that wants an amount writes `gift ? 25 : 0`.\nfunction computeValue(control, type) {\n if (type === \"string\") return computeText(control)\n const box = control.kind === \"checkbox\"\n if (type === \"boolean\") return box ? Boolean(control.el.checked) : computeTruthy(computeText(control))\n if (box) return control.el.checked ? 1 : 0\n const n = Number(computeText(control))\n return Number.isFinite(n) ? n : 0\n}\n\n// Write one reducer output into the control its name resolved to (issue #262),\n// change-guarded. Returns the element to announce with an `input` event, or\n// null when nothing changed. A checkbox takes the result as its checked state\n// and a radio group checks the radio carrying it — neither ever has its value\n// attribute rewritten, which would change what the control SUBMITS. Anything\n// else takes the result as its .value (issue #76).\nfunction computeWrite(root, owns, { el, kind }, domName, value) {\n if (kind === \"checkbox\") {\n const checked = computeTruthy(value)\n if (Boolean(el.checked) === checked) return null\n el.checked = checked\n return el\n }\n if (kind === \"radio\") return computeCheckRadio(root, owns, domName, String(value))\n if (String(value) === el.value) return null\n el.value = value\n return el\n}\n\n// Check the owned radio of a group whose value is `wanted`, unchecking the\n// rest — the same per-radio rule a restored draft applies. A value no radio\n// carries clears the group. Returns the radio that GAINED the check, or, for a\n// cleared group, the one that lost it; null when the selection already matched.\nfunction computeCheckRadio(root, owns, domName, wanted) {\n let announced = null\n for (const el of root.querySelectorAll(`[name=\"${domName}\"]`)) {\n if (el.type !== \"radio\" || !owns(el)) continue\n const checked = el.value === wanted\n if (Boolean(el.checked) === checked) continue\n el.checked = checked\n if (checked) announced = el\n else announced ??= el\n }\n return announced\n}\n\n// Normalize a reducer's reserved $ops output (issue #226): the compute `ops`\n// builder (its .ops list), a raw [[name, args], ...] array, or null/undefined\n// (no effect this pass). An EMPTY list is the same as null — \"nothing to run\"\n// must not latch the rising edge. Anything else warns + is dropped\n// (default-deny, like every other malformed op source). Reducers are trusted\n// app code, but their ops still run through the frozen CLIENT_OPS whitelist.\nfunction computeOpsList(raw) {\n if (raw == null) return null\n const list = Array.isArray(raw) ? raw : raw.ops\n if (Array.isArray(list)) return list.length > 0 ? list : null\n console.warn(\"[phlex-reactive] $ops must be an ops chain or a [[op, args], ...] list — skipped\")\n return null\n}\n\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// Apply a reducer-emitted $ops chain (issue #226) on its RISING EDGE, keyed\n// on CONTENT: fire only when THIS pass's chain differs from the LAST pass's\n// (including from \"absent\"), and only on an event-driven pass. An unchanged\n// chain never re-fires — \"still complete, same submit\" is settled, exactly\n// like the change-guarded field writes. A pass with no $ops re-arms; a pass\n// whose chain CHANGED fires again (per-keystroke focus advance across OTP\n// boxes is a different focus target each time — a deliberately new intent).\n// Ops run through the shared CLIENT_OPS interpreter with runOps's\n// root-scoped target resolution; a missing to: defaults to \"@root\"\n// (hand-written reducer ergonomics).\nfunction applyComputeOps(c, list, eventDriven) {\n const signature = list === null ? null : JSON.stringify(list)\n const fire = signature !== null && signature !== c.opsSig && eventDriven\n c.opsSig = signature\n if (!fire) return\n c.core.applyOps(list, \"@root\")\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.\nfunction parseComputeList(c, attr) {\n const raw = c.root.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.\nfunction parseComputeInputs(c) {\n const raw = c.root.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//\n// Scope-aware (issue #184): under data-reactive-scope, the edited field's DOM\n// name is scoped (order[allowance]) while the declared inputs are BARE\n// (allowance). Strip the scope prefix off the DOM name before comparing, and\n// return the BARE name — so a reducer branching on `changed` sees the same\n// names it declared, scoped or not.\nfunction changedComputeField(c, event, inputs, scope) {\n const target = event?.target\n if (!target?.name || typeof target.closest !== \"function\") return null\n const bare = unscopeName(target.name, scope)\n if (!inputs.includes(bare)) return null\n return c.core.owns(target) ? bare : null\n}\n\n// Strip a leading `scope[…]` wrapper off a DOM field name, returning the bare\n// inner name; a name that isn't wrapped in this scope passes through unchanged.\nfunction unscopeName(name, scope) {\n if (!scope) return name\n const prefix = `${scope}[`\n return name.startsWith(prefix) && name.endsWith(\"]\") ? name.slice(prefix.length, -1) : name\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.\nfunction mirrorText(c, name, value) {\n const text = String(value)\n for (const node of ownedTextNodes(c, 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.\nfunction ownedTextNodes(c, name) {\n const nodes = c.root.querySelectorAll(`[data-reactive-text=\"${name}\"]`)\n return Array.from(nodes).filter((el) => c.core.owns(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.\nfunction applyComputeMirrors(c, result, ownedControl) {\n const mirror = parseComputeMirror(c)\n for (const [name, selectors] of Object.entries(mirror)) {\n const control = name in result ? null : ownedControl(name)\n const value = name in result ? result[name] : control.el ? computeText(control) : undefined\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.\nfunction parseComputeMirror(c) {\n const raw = c.root.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// Whether this root opts into the connect-time compute seed (issue #199).\n// reactive_compute's root binding emits data-reactive-compute-seed=\"true\"; a\n// root without a compute binding (or with the seed opted out) pays one\n// attribute read and never seeds. A quick read, evaluated once per connect.\nfunction computeSeedEnabled(c) {\n return c.root.getAttribute?.(\"data-reactive-compute-seed\") === \"true\"\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.\nfunction recompute(c, event) {\n // Issue #183 — single-pass write set: an `input` event this method dispatched\n // for its OWN output writes is self-marked. Re-running the reducer on it would\n // re-enter from a partially-written DOM (the old mid-loop-dispatch corruption\n // class). Skip the reducer for our own event — but ONLY ours: the marker lives\n // in a per-instance WeakSet, so a genuinely different root's compute event (or\n // a real user edit) is never swallowed. The event still bubbled and fired every\n // OTHER listener (dirty tracking, show bindings, sibling roots) before reaching\n // here; we simply don't recompute a second time from our own write.\n if (event && c.ours.has(event)) return\n\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 = parseComputeInputs(c)\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, so a name read as an input AND written as an output\n // resolves to the SAME element and is queried once.\n //\n // Which element a name resolves to (issue #262, mirroring #showFieldValue): a\n // CHECKBOX wins over the hidden companion Rails renders before it; a radio\n // group resolves to its CHECKED radio (any radio of the group when none is);\n // anything else is first-wins. Resolving first-wins across the board handed\n // the reducer the companion's constant \"0\" and the first radio's value.\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 // Scope (issue #183, mirroring #showFieldValue): a bare compute name `cash`\n // under `data-reactive-scope=\"order\"` resolves as `[name=\"order[cash]\"]`. A\n // name already carrying a bracket (a raw wire name the author passed) is used\n // verbatim — so bracketed literals pass through unscoped.\n const scope = c.root.getAttribute?.(\"data-reactive-scope\") || null\n const scoped = (name) => (scope && !name.includes(\"[\") ? `${scope}[${name}]` : name)\n\n const owns = c.core.ownership()\n const byName = new Map()\n const ownedControl = (name) => {\n const known = byName.get(name)\n if (known) return known\n let radio = null\n let first = null\n let control = null\n for (const el of c.root.querySelectorAll(`[name=\"${scoped(name)}\"]`)) {\n if (!owns(el)) continue\n const kind = el.type // read ONCE per element — see computeText\n if (kind === \"checkbox\" || (kind === \"radio\" && el.checked)) {\n control = { el, kind }\n break\n }\n if (kind === \"radio\") radio ??= el\n else first ??= el\n }\n if (!control) {\n if (radio) control = { el: radio, kind: \"radio\" }\n else control = first ? { el: first, kind: \"\" } : COMPUTE_NO_CONTROL\n }\n byName.set(name, control)\n return control\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 text reading (computeText —\n // a checkbox paints \"true\"/\"false\", issue #262) is written to its owned\n // [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) mirrorText(c, name, computeText(ownedControl(name)))\n\n const key = c.root.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 applyComputeMirrors(c, {}, ownedControl)\n return\n }\n\n const outputs = parseComputeList(c, \"data-reactive-compute-outputs-param\")\n\n // Coerce each input per its declared type (computeValue): \"string\" raw,\n // \"boolean\" a real boolean, else (\"number\", the array-form default) the\n // numeric coercion. A checkbox contributes its CHECKED STATE under every\n // type (issue #262). Reads from the memoized resolver — no re-query.\n const values = {}\n for (const [name, type] of inputPairs) values[name] = computeValue(ownedControl(name), type)\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. ONE run, from the ONE pre-write\n // snapshot above (issue #183): its result drives the whole single-pass write.\n const result = reduce(values, { changed: changedComputeField(c, event, inputs, scope) }) || {}\n\n // The reserved $ops output (issue #226) is CONSUMED here — normalized once,\n // then excluded from every write phase below (it is an op chain, never a\n // field value, text sink, or mirror), and applied as phase 4.\n const reducerOps = computeOpsList(result.$ops)\n\n // Issue #183 — SINGLE-PASS WRITE SET. Ordered phases, so declared output\n // order stops being semantics and a wrong order can no longer corrupt values:\n //\n // 1. BATCH the field writes from the ONE result. Each output name in the\n // allowlist (outputs:) whose owned field's value actually changes is\n // written now (change-guarded) and remembered — but NO `input` event is\n // dispatched yet, so nothing re-enters mid-batch. A checkbox or radio\n // output is written as its CHECKED state (computeWrite, issue #262).\n // 2. PAINT the sinks from the SETTLED values: any owned reactive_text node by\n // presence (issue #183 change #4 — a text node no longer needs its name in\n // outputs:), then the cross-root mirror: ids (issue #159).\n // 3. DISPATCH a self-marked `input` on each changed field. The marker (a\n // per-instance WeakSet) makes recompute skip re-running the reducer for our\n // own write, while the event still fires every OTHER listener (chained\n // repaint, dirty tracking, show bindings, sibling roots).\n // 4. RUN the reducer's $ops chain (issue #226) — rising-edge, event-gated —\n // so its ops (dispatch a completion event, submit the form) always see\n // the fully settled DOM and every chained listener has already run.\n const changedFields = []\n for (const name of outputs) {\n if (name === \"$ops\" || !(name in result)) continue\n const control = ownedControl(name)\n if (!control.el) continue // a non-field output paints as a text sink in phase 2\n const written = computeWrite(c.root, owns, control, scoped(name), result[name])\n if (written) changedFields.push(written) // null = change-guard, unchanged\n }\n\n // Phase 2 — text sinks declare themselves (issue #183 change #4): every result\n // key paints into any owned [data-reactive-text=\"<name>\"] node by PRESENCE,\n // regardless of outputs: membership. Runs from settled field values. A null/\n // undefined result value is SKIPPED (never stringified to \"null\"/\"undefined\") —\n // the same \"no value this pass, don't paint\" filter #applyComputeMirrors uses.\n for (const name of Object.keys(result)) {\n if (name === \"$ops\") continue\n const value = result[name]\n if (value === undefined || value === null) continue\n mirrorText(c, name, value)\n }\n\n // Cross-root text mirrors (issue #159) — AFTER the batch + text sinks, so a\n // mirror keyed on a just-written output paints the settled value.\n applyComputeMirrors(c, result, ownedControl)\n\n // Phase 3 — dispatch the deferred `input` events (issue #183). Real browsers\n // do NOT fire `input` on a programmatic .value write (issue #76), so we do it\n // ourselves, matching the server's set_value + dispatch(\"input\") contract.\n // Each event is SELF-MARKED so our own re-entry skips the reducer (guard at the\n // top of recompute) — but the event still bubbles and fires every other\n // listener. Dispatched AFTER all writes + paints, so a chained listener reads\n // SETTLED values, never a half-written DOM.\n for (const field of changedFields) {\n const inputEvent = new Event(\"input\", { bubbles: true })\n c.ours.add(inputEvent)\n field.dispatchEvent(inputEvent)\n }\n\n // Phase 4 — the reducer's $ops chain (issue #226), after everything settled.\n // Event-gated: a seed/direct recompute() pass (no event) arms the latch but\n // never fires, so a restored/re-rendered complete value can't auto-fire.\n applyComputeOps(c, reducerOps, Boolean(event))\n}\n"
|
|
5
|
+
"// phlex/reactive/features/compute — client-side computes (data binding,\n// issues #104, #159, #183, #199, #262): `recompute`, the connect-time seed,\n// text mirrors and the reducer's $ops. 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// The reducer registry is the compute seam (phlex/reactive/compute), imported\n// here by its bare name — so the runtime no longer imports it, and a page\n// with no compute binding never resolves it. The seed writes output fields a\n// request collects, so with the opt-in client the feature `gates`: a root's\n// requests wait for it. While the module is on its way, the opt-in entry\n// records that the user edited a field (`pending.edited`), and connect() runs\n// ONE recompute then: the reducer reads the inputs as the user left them, so\n// nothing they typed is overwritten, and a `recompute` the window dispatched\n// has run exactly once when it is over.\n//\n// This module never imports the runtime: it reaches a controller through the\n// `core` handle.\nimport { computeReducer } from \"phlex/reactive/compute\"\n\n// controller -> its context: the root, the core handle, and what used to be\n// the controller's own fields for this feature. Short field names: the\n// minifier never renames a property (issue #310).\n// ours the `input` events recompute dispatched for its OWN output\n// writes (issue #183), so re-entering from one skips the reducer\n// (and ONLY one of ours)\n// opsSig the reducer's last $ops chain (issue #226), the rising-edge latch\n// seed the connect-time seed, when the root opts in\n// nested re-seeds when a nested root holding a shared field connects\n// share the input listener for shared fields in nested roots (issue #440)\nconst contexts = new WeakMap()\n\nfunction ctx(controller, core) {\n let c = contexts.get(controller)\n if (!c) contexts.set(controller, (c = { root: controller.element, core, ours: new WeakSet(), opsSig: null }))\n return c\n}\n\nexport function connect(controller, core, _morphed, pending) {\n const c = ctx(controller, core)\n // Connect-time compute seed (issue #199) — ONLY when the root carries a\n // reactive_compute binding that opts in (data-reactive-compute-seed). A\n // freshly-rendered compute root (a first paint, or a server validation-error\n // re-render that replaced the body) computed NOTHING until the first user\n // `input`; apps worked around it by dispatching a synthetic seed `input` on\n // connect, but the compute root is a distinct Stimulus controller that may\n // connect a frame later, so the seed raced its own wiring. Running ONE\n // recompute() HERE — after Stimulus has fully connected the controller and\n // wired the input->recompute delegation — runs the whole single-pass write\n // set (issue #183) synchronously, so every declared output, text sink, and\n // cross-root mirror paints from one reducer result. It is client-only\n // (recompute never enqueues a round trip) and idempotent (change-guarded\n // writes make a re-seed a no-op). A plain replace re-connects and re-seeds;\n // an in-place morph keeps the element CONNECTED and fires no Stimulus\n // lifecycle, so ALSO re-seed on turbo:morph-element. No event is passed, so\n // meta.changed is null — the correct \"no field edited yet\" seed semantics; a\n // convergent reducer's default branch computes the full settled set (see\n // compute.js CONVERGENCE REQUIREMENT).\n const seeds = computeSeedEnabled(c)\n if (seeds) {\n c.seed = () => recompute(c)\n c.root.addEventListener?.(\"turbo:morph-element\", c.seed)\n // Issue #440: a nested root that comes back through a plain replace (a\n // morph bubbles turbo:morph-element above), or restored a draft without an\n // input event, announces itself with a bubbling event; when it holds a\n // shared field this root reads, its new value is one of this root's inputs.\n c.nested = (event) =>\n event.target !== c.root && [...event.target.querySelectorAll(SHARED_FIELD)].some(c.core.bindingOwns) && c.seed()\n for (const type of NESTED_EVENTS) c.root.addEventListener?.(type, c.nested)\n }\n // Issue #440: Stimulus never routes an event from inside a nested reactive\n // root to this root's input->reactive#recompute, yet a control there marked\n // data-reactive-shared-field is one of this root's inputs. Its edits come\n // through this listener instead; an owned field's stay Stimulus's (a second\n // run would be a double one); any other edit costs one attribute read.\n c.share = (event) =>\n event.target?.matches?.(SHARED_FIELD) && !c.core.owns(event.target) && c.core.bindingOwns(event.target) && recompute(c, event)\n c.root.addEventListener?.(\"input\", c.share)\n // The one pass a root that waited for this module owes: its seed, or the\n // recompute an edit in the window asked for.\n if (seeds || pending?.edited) recompute(c)\n}\n\nexport function disconnect(controller) {\n const c = contexts.get(controller)\n if (!c) return\n contexts.delete(controller)\n if (c.seed) c.root.removeEventListener?.(\"turbo:morph-element\", c.seed)\n for (const type of NESTED_EVENTS) c.root.removeEventListener?.(type, c.nested)\n c.root.removeEventListener?.(\"input\", c.share)\n}\n\n// Re-run the seed (for a feature that changed field values after connect:\n// the draft restore).\nexport function seed(controller) {\n contexts.get(controller)?.seed?.()\n}\n\n// The Stimulus action (input->reactive#recompute); the controller's method of\n// the same name hands the event here.\nfunction publicRecompute(controller, core, event) {\n return recompute(ctx(controller, core), event)\n}\nexport { publicRecompute as recompute }\n\n// The marker reactive_shared_field renders (issue #434), and the bubbling\n// events a nested root announces fresh field values with (issue #440).\nconst SHARED_FIELD = \"[data-reactive-shared-field]\"\nconst NESTED_EVENTS = [\"reactive:connect\", \"reactive:persist-restored\"]\n\n// The control for a name nothing owned resolves to.\nconst COMPUTE_NO_CONTROL = Object.freeze({ el: null, kind: \"\" })\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-]*$/\n\n// The TEXT reading of a compute control (issue #262) — what a :string input\n// hands the reducer and what the identity/cross-root mirrors paint. A checkbox\n// reads its CHECKED STATE (\"true\"/\"false\", the strings reactive_show compares\n// against): its .value is a constant — \"1\", \"on\", whatever the markup says —\n// so reading it told the reducer nothing. A radio reads its value only while\n// checked (\"\" otherwise; the resolver hands over the checked radio of a group).\n// Anything else reads .value, as it always has.\n//\n// Every compute helper takes a CONTROL — the { el, kind } record #recompute's\n// resolver builds, kind being \"checkbox\", \"radio\" or \"\" — and never re-reads\n// el.type: the resolver reads it ONCE per name. Re-reading it in each helper\n// cost the 30-input calculator bench ~60% (16.8 → 27 µs/iter, measured).\nfunction computeText({ el, kind }) {\n if (!el) return \"\"\n if (kind === \"checkbox\") return el.checked ? \"true\" : \"false\"\n if (kind === \"radio\") return el.checked ? (el.value ?? \"\") : \"\"\n return el.value ?? \"\"\n}\n\n// Whether a value counts as \"on\" — for a :boolean input read off a control that\n// is not a checkbox, and for an output written INTO a checkbox. A boolean is\n// itself; otherwise \"\", \"0\" and \"false\" are off (what a hidden flag field or a\n// reducer returning 0 means) and anything else is on.\nfunction computeTruthy(value) {\n if (typeof value === \"boolean\") return value\n if (value == null) return false\n const text = String(value)\n return text !== \"\" && text !== \"0\" && text !== \"false\"\n}\n\n// One declared input's value for the reducer, coerced by its declared type\n// (issue #104; checked-state controls issue #262):\n//\n// \"string\" → the text reading, raw (blank/absent → \"\")\n// \"boolean\" → a checkbox's checked state; any other control by computeTruthy\n// \"number\" → a checkbox is 1/0; anything else through Number (blank/NaN → 0,\n// the nanToZero the hand-written calculators use)\n//\n// A checkbox is 1/0 and never Number(its value): a box is a yes/no, and a\n// reducer that wants an amount writes `gift ? 25 : 0`.\nfunction computeValue(control, type) {\n if (type === \"string\") return computeText(control)\n const box = control.kind === \"checkbox\"\n if (type === \"boolean\") return box ? Boolean(control.el.checked) : computeTruthy(computeText(control))\n if (box) return control.el.checked ? 1 : 0\n const n = Number(computeText(control))\n return Number.isFinite(n) ? n : 0\n}\n\n// Write one reducer output into the control its name resolved to (issue #262),\n// change-guarded. Returns the element to announce with an `input` event, or\n// null when nothing changed. A checkbox takes the result as its checked state\n// and a radio group checks the radio carrying it — neither ever has its value\n// attribute rewritten, which would change what the control SUBMITS. Anything\n// else takes the result as its .value (issue #76).\nfunction computeWrite(root, owns, { el, kind }, domName, value) {\n if (kind === \"checkbox\") {\n const checked = computeTruthy(value)\n if (Boolean(el.checked) === checked) return null\n el.checked = checked\n return el\n }\n if (kind === \"radio\") return computeCheckRadio(root, owns, domName, String(value))\n if (String(value) === el.value) return null\n el.value = value\n return el\n}\n\n// Check the owned radio of a group whose value is `wanted`, unchecking the\n// rest — the same per-radio rule a restored draft applies. A value no radio\n// carries clears the group. Returns the radio that GAINED the check, or, for a\n// cleared group, the one that lost it; null when the selection already matched.\nfunction computeCheckRadio(root, owns, domName, wanted) {\n let announced = null\n for (const el of root.querySelectorAll(`[name=\"${domName}\"]`)) {\n if (el.type !== \"radio\" || !owns(el)) continue\n const checked = el.value === wanted\n if (Boolean(el.checked) === checked) continue\n el.checked = checked\n if (checked) announced = el\n else announced ??= el\n }\n return announced\n}\n\n// Normalize a reducer's reserved $ops output (issue #226): the compute `ops`\n// builder (its .ops list), a raw [[name, args], ...] array, or null/undefined\n// (no effect this pass). An EMPTY list is the same as null — \"nothing to run\"\n// must not latch the rising edge. Anything else warns + is dropped\n// (default-deny, like every other malformed op source). Reducers are trusted\n// app code, but their ops still run through the frozen CLIENT_OPS whitelist.\nfunction computeOpsList(raw) {\n if (raw == null) return null\n const list = Array.isArray(raw) ? raw : raw.ops\n if (Array.isArray(list)) return list.length > 0 ? list : null\n console.warn(\"[phlex-reactive] $ops must be an ops chain or a [[op, args], ...] list — skipped\")\n return null\n}\n\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// Apply a reducer-emitted $ops chain (issue #226) on its RISING EDGE, keyed\n// on CONTENT: fire only when THIS pass's chain differs from the LAST pass's\n// (including from \"absent\"), and only on an event-driven pass. An unchanged\n// chain never re-fires — \"still complete, same submit\" is settled, exactly\n// like the change-guarded field writes. A pass with no $ops re-arms; a pass\n// whose chain CHANGED fires again (per-keystroke focus advance across OTP\n// boxes is a different focus target each time — a deliberately new intent).\n// Ops run through the shared CLIENT_OPS interpreter with runOps's\n// root-scoped target resolution; a missing to: defaults to \"@root\"\n// (hand-written reducer ergonomics).\nfunction applyComputeOps(c, list, eventDriven) {\n const signature = list === null ? null : JSON.stringify(list)\n const fire = signature !== null && signature !== c.opsSig && eventDriven\n c.opsSig = signature\n if (!fire) return\n c.core.applyOps(list, \"@root\")\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.\nfunction parseComputeList(c, attr) {\n const raw = c.root.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.\nfunction parseComputeInputs(c) {\n const raw = c.root.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, unless it is a shared field just below, issue #440) AND its name is\n// among the declared compute inputs; anything else (a direct call, an\n// unowned/undeclared target) yields null.\n//\n// Scope-aware (issue #184): under data-reactive-scope, the edited field's DOM\n// name is scoped (order[allowance]) while the declared inputs are BARE\n// (allowance). Strip the scope prefix off the DOM name before comparing, and\n// return the BARE name — so a reducer branching on `changed` sees the same\n// names it declared, scoped or not.\nfunction changedComputeField(c, event, inputs, scope) {\n const target = event?.target\n if (!target?.name || typeof target.closest !== \"function\") return null\n const bare = unscopeName(target.name, scope)\n if (!inputs.includes(bare)) return null\n return c.core.bindingOwns(target) ? bare : null\n}\n\n// Strip a leading `scope[…]` wrapper off a DOM field name, returning the bare\n// inner name; a name that isn't wrapped in this scope passes through unchanged.\nfunction unscopeName(name, scope) {\n if (!scope) return name\n const prefix = `${scope}[`\n return name.startsWith(prefix) && name.endsWith(\"]\") ? name.slice(prefix.length, -1) : name\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.\nfunction mirrorText(c, name, value) {\n const text = String(value)\n for (const node of ownedTextNodes(c, 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.\nfunction ownedTextNodes(c, name) {\n const nodes = c.root.querySelectorAll(`[data-reactive-text=\"${name}\"]`)\n return Array.from(nodes).filter((el) => c.core.owns(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.\nfunction applyComputeMirrors(c, result, ownedControl) {\n const mirror = parseComputeMirror(c)\n for (const [name, selectors] of Object.entries(mirror)) {\n const control = name in result ? null : ownedControl(name)\n const value = name in result ? result[name] : control.el ? computeText(control) : undefined\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.\nfunction parseComputeMirror(c) {\n const raw = c.root.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// Whether this root opts into the connect-time compute seed (issue #199).\n// reactive_compute's root binding emits data-reactive-compute-seed=\"true\"; a\n// root without a compute binding (or with the seed opted out) pays one\n// attribute read and never seeds. A quick read, evaluated once per connect.\nfunction computeSeedEnabled(c) {\n return c.root.getAttribute?.(\"data-reactive-compute-seed\") === \"true\"\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.\nfunction recompute(c, event) {\n // Issue #183 — single-pass write set: an `input` event this method dispatched\n // for its OWN output writes is self-marked. Re-running the reducer on it would\n // re-enter from a partially-written DOM (the old mid-loop-dispatch corruption\n // class). Skip the reducer for our own event — but ONLY ours: the marker lives\n // in a per-instance WeakSet, so a genuinely different root's compute event (or\n // a real user edit) is never swallowed. The event still bubbled and fired every\n // OTHER listener (dirty tracking, show bindings, sibling roots) before reaching\n // here; we simply don't recompute a second time from our own write.\n if (event && c.ours.has(event)) return\n\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 = parseComputeInputs(c)\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, so a name read as an input AND written as an output\n // resolves to the SAME element and is queried once.\n //\n // Which element a name resolves to (issue #262, mirroring #showFieldValue): a\n // CHECKBOX wins over the hidden companion Rails renders before it; a radio\n // group resolves to its CHECKED radio (any radio of the group when none is);\n // anything else is first-wins. Resolving first-wins across the board handed\n // the reducer the companion's constant \"0\" and the first radio's value.\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 // Scope (issue #183, mirroring #showFieldValue): a bare compute name `cash`\n // under `data-reactive-scope=\"order\"` resolves as `[name=\"order[cash]\"]`. A\n // name already carrying a bracket (a raw wire name the author passed) is used\n // verbatim — so bracketed literals pass through unscoped.\n const scope = c.root.getAttribute?.(\"data-reactive-scope\") || null\n const scoped = (name) => (scope && !name.includes(\"[\") ? `${scope}[${name}]` : name)\n\n // Inputs AND outputs resolve through what the bindings read (issue #440):\n // a control marked data-reactive-shared-field just below is this root's\n // too. Text nodes (reactive_text) stay strictly this root's own.\n const owns = c.core.bindingOwnership()\n const byName = new Map()\n const ownedControl = (name) => {\n const known = byName.get(name)\n if (known) return known\n let radio = null\n let first = null\n let control = null\n for (const el of c.root.querySelectorAll(`[name=\"${scoped(name)}\"]`)) {\n if (!owns(el)) continue\n const kind = el.type // read ONCE per element — see computeText\n if (kind === \"checkbox\" || (kind === \"radio\" && el.checked)) {\n control = { el, kind }\n break\n }\n if (kind === \"radio\") radio ??= el\n else first ??= el\n }\n if (!control) {\n if (radio) control = { el: radio, kind: \"radio\" }\n else control = first ? { el: first, kind: \"\" } : COMPUTE_NO_CONTROL\n }\n byName.set(name, control)\n return control\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 text reading (computeText —\n // a checkbox paints \"true\"/\"false\", issue #262) is written to its owned\n // [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) mirrorText(c, name, computeText(ownedControl(name)))\n\n const key = c.root.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 applyComputeMirrors(c, {}, ownedControl)\n return\n }\n\n const outputs = parseComputeList(c, \"data-reactive-compute-outputs-param\")\n\n // Coerce each input per its declared type (computeValue): \"string\" raw,\n // \"boolean\" a real boolean, else (\"number\", the array-form default) the\n // numeric coercion. A checkbox contributes its CHECKED STATE under every\n // type (issue #262). Reads from the memoized resolver — no re-query.\n const values = {}\n for (const [name, type] of inputPairs) values[name] = computeValue(ownedControl(name), type)\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. ONE run, from the ONE pre-write\n // snapshot above (issue #183): its result drives the whole single-pass write.\n const result = reduce(values, { changed: changedComputeField(c, event, inputs, scope) }) || {}\n\n // The reserved $ops output (issue #226) is CONSUMED here — normalized once,\n // then excluded from every write phase below (it is an op chain, never a\n // field value, text sink, or mirror), and applied as phase 4.\n const reducerOps = computeOpsList(result.$ops)\n\n // Issue #183 — SINGLE-PASS WRITE SET. Ordered phases, so declared output\n // order stops being semantics and a wrong order can no longer corrupt values:\n //\n // 1. BATCH the field writes from the ONE result. Each output name in the\n // allowlist (outputs:) whose owned field's value actually changes is\n // written now (change-guarded) and remembered — but NO `input` event is\n // dispatched yet, so nothing re-enters mid-batch. A checkbox or radio\n // output is written as its CHECKED state (computeWrite, issue #262).\n // 2. PAINT the sinks from the SETTLED values: any owned reactive_text node by\n // presence (issue #183 change #4 — a text node no longer needs its name in\n // outputs:), then the cross-root mirror: ids (issue #159).\n // 3. DISPATCH a self-marked `input` on each changed field. The marker (a\n // per-instance WeakSet) makes recompute skip re-running the reducer for our\n // own write, while the event still fires every OTHER listener (chained\n // repaint, dirty tracking, show bindings, sibling roots).\n // 4. RUN the reducer's $ops chain (issue #226) — rising-edge, event-gated —\n // so its ops (dispatch a completion event, submit the form) always see\n // the fully settled DOM and every chained listener has already run.\n const changedFields = []\n for (const name of outputs) {\n if (name === \"$ops\" || !(name in result)) continue\n const control = ownedControl(name)\n if (!control.el) continue // a non-field output paints as a text sink in phase 2\n const written = computeWrite(c.root, owns, control, scoped(name), result[name])\n if (written) changedFields.push(written) // null = change-guard, unchanged\n }\n\n // Phase 2 — text sinks declare themselves (issue #183 change #4): every result\n // key paints into any owned [data-reactive-text=\"<name>\"] node by PRESENCE,\n // regardless of outputs: membership. Runs from settled field values. A null/\n // undefined result value is SKIPPED (never stringified to \"null\"/\"undefined\") —\n // the same \"no value this pass, don't paint\" filter #applyComputeMirrors uses.\n for (const name of Object.keys(result)) {\n if (name === \"$ops\") continue\n const value = result[name]\n if (value === undefined || value === null) continue\n mirrorText(c, name, value)\n }\n\n // Cross-root text mirrors (issue #159) — AFTER the batch + text sinks, so a\n // mirror keyed on a just-written output paints the settled value.\n applyComputeMirrors(c, result, ownedControl)\n\n // Phase 3 — dispatch the deferred `input` events (issue #183). Real browsers\n // do NOT fire `input` on a programmatic .value write (issue #76), so we do it\n // ourselves, matching the server's set_value + dispatch(\"input\") contract.\n // Each event is SELF-MARKED so our own re-entry skips the reducer (guard at the\n // top of recompute) — but the event still bubbles and fires every other\n // listener. Dispatched AFTER all writes + paints, so a chained listener reads\n // SETTLED values, never a half-written DOM.\n for (const field of changedFields) {\n const inputEvent = new Event(\"input\", { bubbles: true })\n c.ours.add(inputEvent)\n field.dispatchEvent(inputEvent)\n }\n\n // Phase 4 — the reducer's $ops chain (issue #226), after everything settled.\n // Event-gated: a seed/direct recompute() pass (no event) arms the latch but\n // never fires, so a restored/re-rendered complete value can't auto-fire.\n applyComputeOps(c, reducerOps, Boolean(event))\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "AAkBA,yBAAS,+
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "5547A787C6231C5664756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -129,6 +129,14 @@ function countMatches(node, selector) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function matchesSelf(node, selector) {
|
|
133
|
+
try {
|
|
134
|
+
return node?.matches?.(selector) === true
|
|
135
|
+
} catch {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
132
140
|
function emitZeroTargetWarn(label, to, scope, hint) {
|
|
133
141
|
if (zeroTargetAlreadyWarned(`${label}|${to}|${scope}`)) return
|
|
134
142
|
console.warn(`[phlex-reactive] ${label} matched zero targets for selector "${to}" (${scope})${hint}`)
|
|
@@ -141,8 +149,8 @@ function emitZeroTargetWarn(label, to, scope, hint) {
|
|
|
141
149
|
export function diagnoseStream(name, args, root) {
|
|
142
150
|
const to = args.to
|
|
143
151
|
if (typeof to !== "string" || to === "" || to === "@root") return
|
|
144
|
-
let hint = ""
|
|
145
|
-
if (root && !args.global) {
|
|
152
|
+
let hint = to === "@trigger" ? " — a server-pushed op has no trigger element" : ""
|
|
153
|
+
if (!hint && root && !args.global) {
|
|
146
154
|
const n = countMatches(globalThis.document, to)
|
|
147
155
|
if (n > 0) hint = ` — it matches ${n} element(s) outside the stream's target root; use global: true`
|
|
148
156
|
}
|
|
@@ -166,9 +174,11 @@ export function diagnose(controller, label, args) {
|
|
|
166
174
|
const root = controller.element
|
|
167
175
|
const to = args.to
|
|
168
176
|
if (typeof to !== "string" || to === "" || to === "@root") return
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
// Issue #442: "@trigger" is no selector — probing it would throw. It matched
|
|
178
|
+
// nothing because this run had no trigger element.
|
|
179
|
+
let hint = to === "@trigger" ? " — this run has no trigger element" : ""
|
|
180
|
+
if (!hint && !args.global) {
|
|
181
|
+
if (matchesSelf(root, to)) {
|
|
172
182
|
hint = " — the selector matches this component's own root, which root-scoped resolution never includes; use to: :root"
|
|
173
183
|
} else if (countMatches(root, to) > 0) {
|
|
174
184
|
hint = " — it matches only inside a nested reactive root (excluded by ownership scoping); use global: true"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var y="phlex-reactive:latency",a=!1;function f(e){if(typeof sessionStorage>"u")return;sessionStorage.setItem("phlex-reactive:latency",String(e))}function g(){if(typeof sessionStorage>"u")return;sessionStorage.removeItem("phlex-reactive:latency"),a=!1}function x(){if(typeof window<"u")window.PhlexReactive={enableLatencySim:f,disableLatencySim:g}}function b(){a=!1}function w(){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(!a)a=!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 c(e,t){try{return e?.querySelectorAll?.(t)?.length??0}catch{return 0}}function h(e,t){try{return e?.matches?.(t)===!0}catch{return!1}}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 S(e,t,o){let n=t.to;if(typeof n!=="string"||n===""||n==="@root")return;let r=n==="@trigger"?" — a server-pushed op has no trigger element":"";if(!r&&o&&!t.global){let s=c(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 v(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 T(e,t,o){let n=e.element,r=o.to;if(typeof r!=="string"||r===""||r==="@root")return;let s=r==="@trigger"?" — this run has no trigger element":"";if(!s&&!o.global)if(h(n,r))s=" — the selector matches this component's own root, which root-scoped resolution never includes; use to: :root";else if(c(n,r)>0)s=" — it matches only inside a nested reactive root (excluded by ownership scoping); use global: true";else{let i=c(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 E(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 p(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 L(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 N(e,t,o){e.streams=p(t),e.tokenRefreshed=o!=null}export{y as LATENCY_KEY,x as attach,w as delay,T as diagnose,S as diagnoseStream,g as disableLatencySim,f as enableLatencySim,v as missingRoot,E as noBinding,N as recordBody,b as resetLatencySim,L as trace};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=B6C25F611EB1EE0B64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=devtools.min.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["features/devtools.js"],
|
|
4
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"
|
|
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 matchesSelf(node, selector) {\n try {\n return node?.matches?.(selector) === true\n } catch {\n return false\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 = to === \"@trigger\" ? \" — a server-pushed op has no trigger element\" : \"\"\n if (!hint && 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 // Issue #442: \"@trigger\" is no selector — probing it would throw. It matched\n // nothing because this run had no trigger element.\n let hint = to === \"@trigger\" ? \" — this run has no trigger element\" : \"\"\n if (!hint && !args.global) {\n if (matchesSelf(root, 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
6
|
],
|
|
7
|
-
"mappings": "AAgCO,IAAM,EAAc,yBAIvB,EAAqB,GAElB,SAAS,CAAgB,CAAC,EAAI,CACnC,GAAI,OAAO,eAAmB,IAAa,OAC3C,eAAe,QARU,yBAQW,OAAO,CAAE,CAAC,EAGzC,SAAS,CAAiB,EAAG,CAClC,GAAI,OAAO,eAAmB,IAAa,OAC3C,eAAe,WAbU,wBAaY,EAKrC,EAAqB,GAUhB,SAAS,CAAM,EAAG,CACvB,GAAI,OAAO,OAAW,IAAa,OAAO,cAAgB,CAAE,mBAAkB,mBAAkB,EAI3F,SAAS,CAAe,EAAG,CAChC,EAAqB,GAUhB,SAAS,CAAK,EAAG,CACtB,GAAI,OAAO,eAAmB,IAAa,OAAO,QAAQ,QAAQ,EAClE,IAAM,EAAK,OAAO,eAAe,QA9CR,wBA8C2B,CAAC,EACrD,GAAI,CAAC,OAAO,SAAS,CAAE,GAAK,GAAM,EAAG,OAAO,QAAQ,QAAQ,EAC5D,GAAI,CAAC,EACH,EAAqB,GACrB,QAAQ,KACN,0EAA0E,uFAE5E,EAEF,OAAO,IAAI,QAAQ,CAAC,IAAY,WAAW,EAAS,CAAE,CAAC,EAmBzD,IAAM,EAAqB,IAAI,QAE/B,SAAS,CAAuB,CAAC,EAAK,CACpC,IAAM,EAAM,WAAW,SACvB,GAAI,CAAC,EAAK,MAAO,GACjB,IAAI,EAAO,EAAmB,IAAI,CAAG,EACrC,GAAI,CAAC,EACH,EAAO,IAAI,IACX,EAAmB,IAAI,EAAK,CAAI,EAElC,GAAI,EAAK,IAAI,CAAG,EAAG,MAAO,GAE1B,OADA,EAAK,IAAI,CAAG,EACL,GAKT,SAAS,CAAY,CAAC,EAAM,EAAU,CACpC,GAAI,CACF,OAAO,GAAM,mBAAmB,CAAQ,GAAG,QAAU,EACrD,KAAM,CACN,MAAO,IAIX,SAAS,CAAkB,CAAC,EAAO,EAAI,EAAO,EAAM,CAClD,GAAI,EAAwB,GAAG,KAAS,KAAM,GAAO,EAAG,OACxD,QAAQ,KAAK,oBAAoB,wCAA4C,OAAQ,KAAS,GAAM,EAO/F,SAAS,CAAc,CAAC,EAAM,EAAM,EAAM,CAC/C,IAAM,EAAK,EAAK,GAChB,GAAI,OAAO,IAAO,UAAY,IAAO,IAAM,IAAO,QAAS,OAC3D,IAAI,EAAO,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "B6C25F611EB1EE0B64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|