import { Signal } from '@preact/signals-core'; /** * Fine-grained signal bindings (KF-294 spike). * * When a `Signal` is interpolated straight into a JSX attribute * (`class={sig}`) or a text child (`{sig}`) INSIDE a `mount()` render, the * JSX runtime stops stringifying it. Instead it emits a marker into the HTML * string and records a binding here; after the string is parsed to DOM, a * wiring pass attaches one `effect` per binding that writes straight to the * live node. A later change to that signal then updates the node WITHOUT * re-running the render function or walking the list reconciler. * * This reuses the "marker in string, wire up after parse" mechanism the * keyed-list reconciler already uses for `` markers. * * TWO SCOPES of binding, with disjoint marker namespaces so their wiring * passes never collide: * * - GLOBAL holes — signals in the static surrounds (outside any `each()` * row). Markers: `data-kfb` attribute / `` comment. Ids come * from the mount render context's counter; wired by `wireBindings()` over * the whole mount root; disposed/re-wired by `mount()` each render. * * - ROW holes — signals inside an `each()` row. Markers: `data-kfbrow` * attribute / `` comment. Ids are row-LOCAL (reset per row) * so they stay stable and collision-free as rows are inserted/removed/moved. * Captured per row by `captureRowBindings()`, carried on the list segment * item, and wired/disposed by the list reconciler at each row node's * create/remove — so a binding's lifetime tracks its row node's lifetime, * and row reorders (which reuse the same node) are free. * * Outside a `mount()` render (SSR / `SafeHtml.toString()`) neither scope is * active: the runtime snapshots `signal.value` and emits no markers, so server * output is correct and legacy `.toString()` callers are unaffected. * * Module-level mutable state note: `context` / `rowSink` (plus `rowCounter`, * the row-hole id counter that resets with each row capture) are a third * sanctioned module-level mutable location (alongside `store.ts:REGISTRY` and * `each.ts:context`). They hold the current render's binding sinks and are set * / cleared by `mount()` and `each()` around the render calls. The only other * module-level container here is `insertedTextNodes`, a WeakMap keyed on text * marker comments — a pure cache whose entries die with their nodes (GC-tied * lifetime), so it carries no cross-render semantics. */ interface AttrBinding { kind: 'attr'; id: string; attr: string; signal: Signal; } interface TextBinding { kind: 'text'; id: string; signal: Signal; } type Binding = AttrBinding | TextBinding; export type { Binding as B };