@wcstack/signals 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,743 @@
1
+ import { e as effect, o as onCleanup, g as isDev, h as hasOwner, w as warnDev, s as signal, a as createRoot } from './core-DRLhLwcb.esm.js';
2
+ export { D as DisposedError, b as bindNode, c as computed, f as flushSync, i as isDisposedError, n as nodeSource, r as resource, d as streamResource } from './core-DRLhLwcb.esm.js';
3
+
4
+ // Fine-grained hyperscript (v1 design notes). The "step before JSX" (docs §4-1).
5
+ //
6
+ // `h(tag, props, ...children)` is the classic JSX factory shape, but it does NOT
7
+ // build a virtual DOM and re-render. It creates a REAL DOM node once; any prop or
8
+ // child given as a function (thunk) or signal is wired to a targeted `effect`, so
9
+ // only that one binding updates when its signals change (Solid-style). This is
10
+ // what keeps the package lightweight: no reconciler is shipped.
11
+ //
12
+ // JSX is intentionally NOT shipped. A consumer who wants it sets, in their own
13
+ // tsconfig, `jsxFactory: "h"` + `jsxFragmentFactory: "Fragment"` (classic runtime)
14
+ // — opting into a build step is their choice; the buildless path is calling `h`
15
+ // directly. See docs/signals-state-design.md §4-1.
16
+ //
17
+ // OWNERSHIP: effects created here are owned by the enclosing reactive scope.
18
+ // A reactive child's effect owns the prop/child effects of the subtree it builds,
19
+ // so rebuilding that subtree disposes the previous one's effects (no leak). Mount
20
+ // an app under `createRoot` so the whole tree can be torn down on unmount; a
21
+ // dynamic child establishes its own scope automatically (it IS an effect).
22
+ const Fragment = Symbol("signals.Fragment");
23
+ function isReadSignal(value) {
24
+ return (typeof value === "object" &&
25
+ value !== null &&
26
+ typeof value.get === "function" &&
27
+ typeof value.peek === "function");
28
+ }
29
+ function h(tag, props, ...children) {
30
+ if (tag === Fragment) {
31
+ const frag = document.createDocumentFragment();
32
+ appendChildren(frag, children);
33
+ return frag;
34
+ }
35
+ if (typeof tag === "function") {
36
+ const result = tag({ ...(props ?? {}), children });
37
+ return Array.isArray(result) ? wrapFragment(result) : result;
38
+ }
39
+ const el = createElement(tag);
40
+ if (props) {
41
+ for (const key in props) {
42
+ bindProp(el, key, props[key]);
43
+ }
44
+ }
45
+ appendChildren(el, children);
46
+ return el;
47
+ }
48
+ const SVG_NS = "http://www.w3.org/2000/svg";
49
+ // Tags created in the SVG namespace. Ambiguous names shared with HTML (`a`,
50
+ // `title`, `script`, `style`) are intentionally EXCLUDED so ordinary HTML usage is
51
+ // not broken; an SVG `<a>` / `<title>` must be created in an explicitly-namespaced
52
+ // way by the caller. SVG DOM props are largely read-only, so `setProp`'s
53
+ // settable-property check routes SVG attributes through `setAttribute` for free.
54
+ const SVG_TAGS = new Set([
55
+ "svg", "g", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon",
56
+ "text", "tspan", "defs", "use", "symbol", "marker", "linearGradient",
57
+ "radialGradient", "stop", "clipPath", "mask", "pattern", "image", "foreignObject",
58
+ ]);
59
+ function createElement(tag) {
60
+ return SVG_TAGS.has(tag)
61
+ ? document.createElementNS(SVG_NS, tag)
62
+ : document.createElement(tag);
63
+ }
64
+ /** Append `child` into `container`, resolving fragments/arrays. */
65
+ function render(child, container) {
66
+ appendChild(container, child);
67
+ return container;
68
+ }
69
+ function wrapFragment(children) {
70
+ const frag = document.createDocumentFragment();
71
+ appendChildren(frag, children);
72
+ return frag;
73
+ }
74
+ // --- props ------------------------------------------------------------------
75
+ // Attribute names whose corresponding JS DOM property differs. `class` is handled
76
+ // separately (it has reactive-falsy semantics in setProp), so it is not listed here.
77
+ const ATTR_TO_PROP = {
78
+ for: "htmlFor",
79
+ tabindex: "tabIndex",
80
+ colspan: "colSpan",
81
+ rowspan: "rowSpan",
82
+ readonly: "readOnly",
83
+ maxlength: "maxLength",
84
+ minlength: "minLength",
85
+ contenteditable: "contentEditable",
86
+ };
87
+ // True only if `key` resolves to a WRITABLE property on the element or its
88
+ // prototype chain (a data property with `writable !== false`, or an accessor with a
89
+ // setter). `key in el` is not enough: it also matches read-only members like
90
+ // `firstChild` / `childNodes`, whose assignment throws in strict mode. The walk
91
+ // starts at `el` itself so own instance fields (e.g. a custom element's properties)
92
+ // are still matched, exactly as the previous `key in el` did — just without the
93
+ // read-only false positives.
94
+ //
95
+ // MEMOIZATION: the prototype-chain walk runs on every bindProp/setProp. The result
96
+ // depends only on the element's PROTOTYPE (its class) and the key, since DOM props
97
+ // live on the prototype — not on per-instance own fields, which this library never
98
+ // adds dynamically per element. So cache by `(prototype, key)`: a WeakMap keyed by
99
+ // prototype (no leak — entries die with the class) holding a Map of key→result.
100
+ const settableCache = new WeakMap();
101
+ function computeSettable(el, key) {
102
+ let obj = el;
103
+ while (obj !== null) {
104
+ const desc = Object.getOwnPropertyDescriptor(obj, key);
105
+ if (desc) {
106
+ return "set" in desc ? typeof desc.set === "function" : desc.writable !== false;
107
+ }
108
+ obj = Object.getPrototypeOf(obj);
109
+ }
110
+ return false;
111
+ }
112
+ function isSettableProperty(el, key) {
113
+ // A real Element always has a prototype (HTMLElement.prototype → … → Object), so
114
+ // `getPrototypeOf` is non-null here; use it as the cache key.
115
+ const proto = Object.getPrototypeOf(el);
116
+ let byKey = settableCache.get(proto);
117
+ if (byKey === undefined) {
118
+ byKey = new Map();
119
+ settableCache.set(proto, byKey);
120
+ }
121
+ let result = byKey.get(key);
122
+ if (result === undefined) {
123
+ result = computeSettable(el, key);
124
+ byKey.set(key, result);
125
+ }
126
+ return result;
127
+ }
128
+ function bindProp(el, key, value) {
129
+ // Event handlers (`onClick` → "click") are special-cased BEFORE the reactive
130
+ // check: a function here is the listener, not a thunk to track.
131
+ if (/^on[A-Z]/.test(key) && typeof value === "function") {
132
+ const type = key.slice(2).toLowerCase();
133
+ const listener = value;
134
+ el.addEventListener(type, listener);
135
+ // Remove the listener when the owning scope is torn down (e.g. a dynamic
136
+ // child that rebuilds this subtree), so handlers don't accumulate.
137
+ onCleanup(() => el.removeEventListener(type, listener));
138
+ return;
139
+ }
140
+ if (typeof value === "function") {
141
+ effect(() => setProp(el, key, value()));
142
+ return;
143
+ }
144
+ if (isReadSignal(value)) {
145
+ effect(() => setProp(el, key, value.get()));
146
+ return;
147
+ }
148
+ setProp(el, key, value);
149
+ }
150
+ function setProp(el, key, value) {
151
+ if (key === "style") {
152
+ setStyle(el, value);
153
+ return;
154
+ }
155
+ if (key === "class" || key === "className") {
156
+ // Set the `class` ATTRIBUTE, not the `className` property. On an SVGElement
157
+ // `className` is a read-only `SVGAnimatedString`, so assigning it throws in
158
+ // strict mode (ESM is always strict) — `setAttribute("class", …)` works for both
159
+ // HTML and SVG. Treat false like null → empty class (consistent with the
160
+ // attribute path's false-removes-it rule), so `() => cond && "active"` yields ""
161
+ // (not "false") when the condition is falsy.
162
+ // STRING-ONLY: a non-null/non-false value is coerced via String(value). Array
163
+ // (`["a", "b"]`) and object (`{ active: true }`) class forms are NOT supported —
164
+ // they would stringify to "a,b" / "[object Object]". Compose the class string in
165
+ // the binding itself (e.g. `() => [a, b].join(" ")`) and pass a string. This keeps
166
+ // the core free of a class-merging convention.
167
+ el.setAttribute("class", value == null || value === false ? "" : String(value));
168
+ return;
169
+ }
170
+ // Remap the handful of attribute names whose JS property differs, so `for` /
171
+ // `tabindex` / `colspan` etc. reach the right property instead of falling through
172
+ // to setAttribute (which works for some but not e.g. `htmlFor`).
173
+ const propKey = ATTR_TO_PROP[key] ?? key;
174
+ if (isSettableProperty(el, propKey)) {
175
+ // Known, WRITABLE DOM property (value, checked, disabled, id, htmlFor, ...).
176
+ // The settability check excludes read-only members (firstChild, childNodes, …)
177
+ // that `key in el` would wrongly accept — assigning to those throws in strict
178
+ // mode; we fall through to the attribute path for them instead.
179
+ //
180
+ // NORMALIZATION: null/undefined are coerced to "" before assignment. Without
181
+ // this, a STRING prop (id/title/value/src) given a reactive null/undefined
182
+ // lands as the literal "null"/"undefined" (e.g. img.src="null" fires a real
183
+ // request) — a correctness footgun. "" clears the prop instead. Safe for
184
+ // non-string props too: boolean props coerce ""→false (same as null), numeric
185
+ // props coerce ""→0 (same as null). We do NOT normalize `false`:
186
+ // `el.disabled = false` is the intended boolean clear.
187
+ el[propKey] = value == null ? "" : value;
188
+ return;
189
+ }
190
+ if (value == null || value === false) {
191
+ el.removeAttribute(key);
192
+ return;
193
+ }
194
+ if (value === true) {
195
+ el.setAttribute(key, "");
196
+ return;
197
+ }
198
+ el.setAttribute(key, String(value));
199
+ }
200
+ function setStyle(el, value) {
201
+ if (value == null) {
202
+ el.removeAttribute("style");
203
+ return;
204
+ }
205
+ if (typeof value === "string") {
206
+ el.style.cssText = value;
207
+ return;
208
+ }
209
+ // Object form: the object fully describes the style. Reset first so keys present
210
+ // in a previous run but absent now are removed — otherwise a reactive style that
211
+ // drops a key (e.g. {color,fontWeight} → {color}) would leave the stale property
212
+ // applied. The object is the source of truth, so wiping cssText is correct here.
213
+ el.style.cssText = "";
214
+ const style = el.style;
215
+ const obj = value;
216
+ for (const k in obj) {
217
+ // A key with a hyphen is either kebab-case (`font-weight`) or a CSS custom
218
+ // property (`--accent`). Neither works via property assignment (`style[k]` would
219
+ // just set an inert expando), so route them through setProperty. camelCase keys
220
+ // (`fontWeight`) keep the faster property path.
221
+ if (k.includes("-")) {
222
+ style.setProperty(k, obj[k]);
223
+ }
224
+ else {
225
+ style[k] = obj[k];
226
+ }
227
+ }
228
+ }
229
+ // --- children ---------------------------------------------------------------
230
+ function appendChildren(parent, children) {
231
+ for (const child of children) {
232
+ appendChild(parent, child);
233
+ }
234
+ }
235
+ function appendChild(parent, child) {
236
+ if (child == null || typeof child === "boolean") {
237
+ return; // null / undefined / boolean render nothing
238
+ }
239
+ if (Array.isArray(child)) {
240
+ for (const c of child) {
241
+ appendChild(parent, c);
242
+ }
243
+ return;
244
+ }
245
+ if (isListView(child)) {
246
+ // A keyed list (For / Index): it manages its own anchored region with in-place
247
+ // reconciliation, so it does NOT go through the wholesale insertReactive path.
248
+ child.mount(parent);
249
+ return;
250
+ }
251
+ if (child instanceof Node) {
252
+ parent.appendChild(child);
253
+ return;
254
+ }
255
+ if (typeof child === "function") {
256
+ insertReactive(parent, child);
257
+ return;
258
+ }
259
+ if (isReadSignal(child)) {
260
+ insertReactive(parent, () => child.get());
261
+ return;
262
+ }
263
+ parent.appendChild(document.createTextNode(String(child)));
264
+ }
265
+ /**
266
+ * A reactive insertion point. An anchor comment marks the position; on each run
267
+ * the previous nodes are removed and the freshly-resolved nodes inserted before
268
+ * the anchor. Using `anchor.parentNode` (not the original `parent`) keeps it
269
+ * correct after the subtree is moved/mounted elsewhere.
270
+ *
271
+ * PRECONDITION: the Nodes returned by `accessor` are owned by this insertion point
272
+ * — do not move them to another parent externally. On the next run they are
273
+ * removed from wherever they currently are (`node.parentNode?.removeChild`), so an
274
+ * externally-moved node would be yanked out of its new home without warning.
275
+ *
276
+ * OWNERSHIP: the `effect` below is owned by the enclosing reactive scope at call
277
+ * time (the `currentOwner`). Used under `createRoot` / `SignalsElement` (the module
278
+ * header's mount contract) it is disposed on teardown. Calling `h`/`render` with a
279
+ * reactive child OUTSIDE any owner (no `createRoot`/`SignalsElement`) leaves this
280
+ * effect un-owned and therefore never cleaned up — by design, per that contract.
281
+ */
282
+ function insertReactive(parent, accessor) {
283
+ if (isDev() && !hasOwner()) {
284
+ // A reactive child inserted with no enclosing owner: the effect below is never
285
+ // disposed (the module header documents this as by-design, but it is a frequent
286
+ // accidental leak). Warn once in dev so the un-owned mount is visible.
287
+ warnDev("UNOWNED_INSERT", "", "reactive child inserted with no owner — its update effect will never be " +
288
+ "disposed and may leak. Mount under createRoot / SignalsElement.");
289
+ }
290
+ const anchor = document.createComment("");
291
+ parent.appendChild(anchor);
292
+ let current = [];
293
+ effect(() => {
294
+ const value = accessor();
295
+ // FAST PATH: a plain text result that replaces a plain text result. The common
296
+ // reactive child is a string/number thunk (`() => \`count: ${n.get()}\``); the
297
+ // wholesale path would remove the old Text node, allocate a new one, and re-insert
298
+ // it on every change. When BOTH the previous and next renders are a single Text
299
+ // node, mutate the existing node's `data` in place instead — no allocation, no
300
+ // remove/insert, and the same DOM node identity is preserved across updates.
301
+ if ((typeof value === "string" || typeof value === "number") && current.length === 1) {
302
+ const node = current[0];
303
+ if (node.nodeType === 3 /* TEXT_NODE */) {
304
+ node.data = String(value);
305
+ return;
306
+ }
307
+ }
308
+ const next = toNodes(value);
309
+ for (const node of current) {
310
+ node.parentNode?.removeChild(node);
311
+ }
312
+ const host = anchor.parentNode;
313
+ if (host) {
314
+ for (const node of next) {
315
+ host.insertBefore(node, anchor);
316
+ }
317
+ }
318
+ current = next;
319
+ });
320
+ // On teardown, drop the references to the inserted Nodes. The disposed effect's
321
+ // closure still captures `current`; if anything retains the effect after disposal
322
+ // (e.g. a parent owner kept alive longer than the subtree), those Nodes would be
323
+ // pinned via that closure even though they have left the DOM. Clearing the array
324
+ // lets them be GC'd as soon as the enclosing owner tears this insertion point down.
325
+ onCleanup(() => {
326
+ current = [];
327
+ });
328
+ }
329
+ function isListView(value) {
330
+ return typeof value === "object" && value !== null && value.__wcsList === true;
331
+ }
332
+ function readList(list) {
333
+ return isReadSignal(list) ? () => list.get() : list;
334
+ }
335
+ // --- LIS-based DOM reorder ---------------------------------------------------
336
+ //
337
+ // `reconcileOrder` brings the live DOM under `host` (the contiguous region ending
338
+ // at `anchor`) into `order` — the desired final node sequence — using the FEWEST
339
+ // `insertBefore` moves, while producing EXACTLY the order a naive full re-place
340
+ // would. The naive approach (walk and insert every out-of-place node) does up to
341
+ // O(n) DOM moves on a full reverse or a head insert; here we instead keep the
342
+ // largest set of reused rows that are ALREADY in the right relative order, and
343
+ // move only the others (Solid's reconcileArrays / udomdiff idea).
344
+ //
345
+ // HOW: `prevOrder` is the node sequence the previous run left in the DOM. For each
346
+ // node in the new `order`, a reused row maps to its index in `prevOrder`; a freshly
347
+ // created row has no previous position. The longest increasing subsequence (LIS)
348
+ // of those previous indices marks the reused rows that need NOT move (their pairwise
349
+ // relative order is already correct). Every other node — fresh ones, and reused
350
+ // ones outside the LIS — is inserted before its already-correct successor, walking
351
+ // back-to-front so the "successor" reference is always a settled node or the anchor.
352
+ /**
353
+ * Indices into `arr` (with value -1 meaning "no previous position") whose values
354
+ * form a longest strictly-increasing subsequence. Entries equal to -1 are treated
355
+ * as never part of the subsequence (a fresh node always moves). Classic
356
+ * patience-sorting LIS in O(n log n); returns the chosen indices in ascending order.
357
+ */
358
+ function longestIncreasingSubsequence(arr) {
359
+ const n = arr.length;
360
+ // `tails[k]` = index (into arr) of the smallest tail of an increasing
361
+ // subsequence of length k+1. `prev[i]` = predecessor index, to rebuild the chain.
362
+ const tails = [];
363
+ const prev = new Array(n).fill(-1);
364
+ for (let i = 0; i < n; i++) {
365
+ const value = arr[i];
366
+ if (value < 0) {
367
+ continue; // fresh node: not eligible for the stable subsequence
368
+ }
369
+ // Binary search for the first tail whose value is >= this value, and replace it
370
+ // (or extend the tails when this value beats every tail).
371
+ let lo = 0;
372
+ let hi = tails.length;
373
+ while (lo < hi) {
374
+ const mid = (lo + hi) >> 1;
375
+ if (arr[tails[mid]] < value) {
376
+ lo = mid + 1;
377
+ }
378
+ else {
379
+ hi = mid;
380
+ }
381
+ }
382
+ if (lo > 0) {
383
+ prev[i] = tails[lo - 1];
384
+ }
385
+ tails[lo] = i;
386
+ }
387
+ // Rebuild the index chain from the longest tail backwards.
388
+ const result = [];
389
+ let k = tails.length > 0 ? tails[tails.length - 1] : -1;
390
+ while (k >= 0) {
391
+ result.push(k);
392
+ k = prev[k];
393
+ }
394
+ result.reverse();
395
+ return result;
396
+ }
397
+ function reconcileOrder(host, anchor, order, prevOrder) {
398
+ const n = order.length;
399
+ if (n === 0) {
400
+ return; // nothing to place (removals already happened)
401
+ }
402
+ // Map each previous node to its previous position, so a node in `order` can look
403
+ // up where it used to be. A node absent from this map is FRESH (created this run,
404
+ // or the first run where prevOrder is empty) and is encoded as -1 — it always moves.
405
+ const prevIndex = new Map();
406
+ for (let i = 0; i < prevOrder.length; i++) {
407
+ prevIndex.set(prevOrder[i], i);
408
+ }
409
+ // previous-position sequence aligned to the new order. -1 marks a fresh node.
410
+ const seq = new Array(n);
411
+ for (let i = 0; i < n; i++) {
412
+ const p = prevIndex.get(order[i]);
413
+ seq[i] = p === undefined ? -1 : p;
414
+ }
415
+ // Rows whose relative order is already correct: leave them in place, move the rest.
416
+ const lis = longestIncreasingSubsequence(seq);
417
+ let lisPtr = lis.length - 1;
418
+ // Walk back-to-front. `ref` is the node that must follow the one being placed; it
419
+ // starts at the anchor (the region's tail) and becomes each placed node. A node in
420
+ // the LIS is the maximal already-correctly-ordered set, so it is left untouched
421
+ // (only adopted as the new `ref`). Every OTHER node is, by construction, out of
422
+ // position relative to its settled successor and is inserted before `ref` — no
423
+ // "is it already in place?" guard is needed (it never is, unlike the old
424
+ // full-walk reconciler which visited correctly-placed nodes too).
425
+ let ref = anchor;
426
+ for (let i = n - 1; i >= 0; i--) {
427
+ const node = order[i];
428
+ if (lisPtr >= 0 && lis[lisPtr] === i) {
429
+ lisPtr--; // stable: do not move
430
+ }
431
+ else {
432
+ host.insertBefore(node, ref);
433
+ }
434
+ ref = node;
435
+ }
436
+ }
437
+ // Dev-only key sanity check (no effect in production — guarded by `isDev()` at the
438
+ // call site so this is never reached when dev is off). Flags the silent-failure key
439
+ // shapes: a nullish key (NaN / null / undefined collide under SameValueZero, so two
440
+ // such items map to one row and a row is silently dropped), and — when no explicit
441
+ // `key` was given — a non-primitive item used as its own identity key (every render
442
+ // gets a fresh object reference, so the row is rebuilt instead of reused, silently
443
+ // dropping per-row state). Warns once per distinct shape (deduped in warnDev).
444
+ function checkKeyDev(key, item, hasExplicitKey) {
445
+ if (key == null || (typeof key === "number" && Number.isNaN(key))) {
446
+ warnDev("NULLISH_KEY", String(key), "For: a key is null/undefined/NaN. Such keys collide under SameValueZero, " +
447
+ "so rows silently merge/drop. Provide a stable unique `key`.", { key });
448
+ return;
449
+ }
450
+ if (!hasExplicitKey && (typeof key === "object" || typeof key === "function")) {
451
+ warnDev("NON_PRIMITIVE_KEY", typeof key, "For: a non-primitive item is used as its own key (no `key` option given). " +
452
+ "Object identity changes across renders, so every row is rebuilt and per-row " +
453
+ "state is lost. Pass `{ key: item => item.id }`.", { item });
454
+ }
455
+ }
456
+ function For(list, each, options) {
457
+ const read = readList(list);
458
+ const keyOf = options?.key;
459
+ return {
460
+ __wcsList: true,
461
+ mount(parent) {
462
+ const anchor = document.createComment("for");
463
+ parent.appendChild(anchor);
464
+ let rows = new Map();
465
+ // The node order placed by the previous reconcile run. Empty on first run.
466
+ // Used to find which reused rows already sit in increasing relative order so
467
+ // only the rest are moved (LIS reorder, see reconcileOrder).
468
+ let prevOrder = [];
469
+ const make = (item, i) => {
470
+ const idx = signal(i);
471
+ let node;
472
+ const dispose = createRoot((d) => {
473
+ node = each(item, () => idx.get());
474
+ return d;
475
+ });
476
+ return { node, dispose, idx };
477
+ };
478
+ effect(() => {
479
+ const items = read() ?? [];
480
+ // Key-uniqueness PRE-PASS (no side effects): throwing here, before any row is
481
+ // created or moved, keeps a duplicate-key error from leaving half-built rows
482
+ // orphaned (make() runs createRoot) and the DOM mid-reorder. Keys are reused
483
+ // below so keyOf runs once per item.
484
+ const keys = [];
485
+ const seen = new Set();
486
+ const dev = isDev();
487
+ for (let i = 0; i < items.length; i++) {
488
+ const k = keyOf ? keyOf(items[i], i) : items[i];
489
+ if (dev) {
490
+ checkKeyDev(k, items[i], keyOf !== undefined);
491
+ }
492
+ if (seen.has(k)) {
493
+ if (dev) {
494
+ // The throw below still halts (production behaviour unchanged); the warn
495
+ // adds the offending key + a pointer to the cause so the silent
496
+ // "updates stopped" symptom is diagnosable.
497
+ warnDev("DUPLICATE_KEY", String(k), `For: duplicate key "${String(k)}" at index ${i} — keys must be unique. ` +
498
+ "Rendering throws and the list stops updating until keys are fixed.", { key: k, index: i });
499
+ }
500
+ throw new Error(`For: duplicate key "${String(k)}" — keys must be unique.`);
501
+ }
502
+ seen.add(k);
503
+ keys.push(k);
504
+ }
505
+ const next = new Map();
506
+ const order = [];
507
+ // Rows freshly created in THIS run (not reused from `rows`). If a later
508
+ // `make()` — i.e. a user `each` — throws mid-loop, the rows already built
509
+ // here are owned by their own detached `createRoot` and are not yet tracked
510
+ // in any surviving map, so they would leak. Dispose them before re-throwing,
511
+ // leaving the old `rows` map untouched (it is recoverable on the next run).
512
+ const fresh = [];
513
+ try {
514
+ for (let i = 0; i < items.length; i++) {
515
+ const k = keys[i];
516
+ let row = rows.get(k);
517
+ if (row) {
518
+ if (row.idx.peek() !== i) {
519
+ row.idx.set(i); // position changed — refresh the index accessor
520
+ }
521
+ }
522
+ else {
523
+ row = make(items[i], i);
524
+ fresh.push(row);
525
+ }
526
+ next.set(k, row);
527
+ order.push(row.node);
528
+ }
529
+ }
530
+ catch (err) {
531
+ for (const row of fresh) {
532
+ row.dispose();
533
+ }
534
+ throw err;
535
+ }
536
+ // Dispose rows whose key vanished. `remove()` no-ops if already detached.
537
+ for (const [k, row] of rows) {
538
+ if (!next.has(k)) {
539
+ row.node.remove();
540
+ row.dispose();
541
+ }
542
+ }
543
+ // Reorder relative to the LIVE parent (`anchor.parentNode`, not the mount
544
+ // arg) so the region stays correct after being moved (e.g. built inside a
545
+ // Fragment, then appended elsewhere). `host` is null only if the anchor was
546
+ // detached without disposing this scope (a misuse) — then there is nothing
547
+ // to place into.
548
+ const host = anchor.parentNode;
549
+ if (host) {
550
+ reconcileOrder(host, anchor, order, prevOrder);
551
+ }
552
+ // Snapshot this run's final order so the NEXT run's reconcile can detect
553
+ // which rows kept their relative position (the LIS basis below). Capturing
554
+ // the array we just placed avoids re-deriving it from the map.
555
+ prevOrder = order;
556
+ rows = next;
557
+ });
558
+ onCleanup(() => {
559
+ for (const row of rows.values()) {
560
+ row.dispose();
561
+ }
562
+ rows.clear();
563
+ });
564
+ },
565
+ };
566
+ }
567
+ function Index(list, each) {
568
+ const read = readList(list);
569
+ return {
570
+ __wcsList: true,
571
+ mount(parent) {
572
+ const anchor = document.createComment("index");
573
+ parent.appendChild(anchor);
574
+ const rows = [];
575
+ effect(() => {
576
+ const items = read() ?? [];
577
+ // Live parent (see `For`), null only if the anchor was detached undisposed.
578
+ const host = anchor.parentNode;
579
+ for (let i = 0; i < items.length; i++) {
580
+ if (i < rows.length) {
581
+ // Slot reused: just push the new value into the row's item signal. The
582
+ // signal's equality guard skips the update when the value is unchanged.
583
+ rows[i].item.set(items[i]);
584
+ }
585
+ else {
586
+ const item = signal(items[i]);
587
+ let node;
588
+ const dispose = createRoot((d) => {
589
+ node = each(() => item.get(), i);
590
+ return d;
591
+ });
592
+ rows.push({ node, dispose, item });
593
+ // Positions never move for index keying, so append at the tail (before
594
+ // the anchor) in order. ORDERING CONTRACT (shared with For and the other
595
+ // anchored insertions): the list owns the contiguous region ENDING at its
596
+ // anchor comment. New rows go immediately before the anchor, so the list's
597
+ // own rows always stay correctly ordered and grouped, and static siblings
598
+ // before the anchor (or another list with its own anchor) are never
599
+ // disturbed. Do NOT splice unrelated nodes into the middle of this region.
600
+ host?.insertBefore(node, anchor);
601
+ }
602
+ }
603
+ // Trailing slots that no longer exist: dispose and drop.
604
+ if (items.length < rows.length) {
605
+ for (let i = items.length; i < rows.length; i++) {
606
+ rows[i].node.remove();
607
+ rows[i].dispose();
608
+ }
609
+ rows.length = items.length;
610
+ }
611
+ });
612
+ onCleanup(() => {
613
+ for (const row of rows) {
614
+ row.dispose();
615
+ }
616
+ rows.length = 0;
617
+ });
618
+ },
619
+ };
620
+ }
621
+ // Memoized base class. Built lazily on first access so the `./dom` module can be
622
+ // EVALUATED in a non-DOM environment (SSR pre-pass, a worker without a DOM shim)
623
+ // without a top-level `class … extends HTMLElement` blowing up with
624
+ // `ReferenceError: HTMLElement is not defined`. The class body is only constructed
625
+ // when the consumer actually reaches for the base — at which point a DOM is required
626
+ // and its absence yields a clear, actionable error instead of a raw ReferenceError.
627
+ let cachedBase = null;
628
+ /**
629
+ * Build (once, memoized) the `SignalsElement` lifecycle base class. Resolves
630
+ * `HTMLElement` at CALL TIME, so importing `@wcstack/signals/dom` no longer requires
631
+ * a DOM at module-evaluation time — only *calling* this (or subclassing
632
+ * {@link SignalsElement}) does.
633
+ *
634
+ * Contract:
635
+ * - The `.` entry (`@wcstack/signals`) is non-DOM: it evaluates and runs in SSR,
636
+ * Node, and Web Workers with no DOM globals.
637
+ * - The `./dom` entry (`@wcstack/signals/dom`) may now also be *evaluated* without a
638
+ * DOM (so an SSR pre-pass that imports it for the headless re-exports does not
639
+ * crash). The DOM-touching surface — `h`/`render`/`For`/`Index` and this base —
640
+ * still requires DOM globals when actually used.
641
+ *
642
+ * @throws a clear `Error` (not a raw `ReferenceError`) when no `HTMLElement` global is
643
+ * present, naming the offending entry so the failure is self-explanatory.
644
+ */
645
+ function createSignalsElement() {
646
+ if (cachedBase !== null) {
647
+ return cachedBase;
648
+ }
649
+ if (typeof HTMLElement === "undefined") {
650
+ throw new Error("@wcstack/signals/dom: SignalsElement requires a DOM (HTMLElement is not defined). " +
651
+ "The `./dom` entry's element/render surface is browser-only; in SSR/Node/worker " +
652
+ "code import the headless core from `@wcstack/signals` instead, or inject a DOM " +
653
+ "(e.g. happy-dom) before constructing custom elements.");
654
+ }
655
+ class SignalsElementBase extends HTMLElement {
656
+ _dispose = null;
657
+ /** Mount target. Override to return a shadow root; default is light DOM. */
658
+ getMountPoint() {
659
+ return this;
660
+ }
661
+ connectedCallback() {
662
+ if (this._dispose !== null) {
663
+ return; // already mounted (defensive against a redundant connect)
664
+ }
665
+ const mount = this.getMountPoint();
666
+ // If render() throws, createRoot disposes any effects it built before the throw,
667
+ // so nothing leaks; _dispose stays null (the element is simply not mounted) and
668
+ // a later disconnectedCallback safely no-ops. The error propagates to the caller.
669
+ this._dispose = createRoot((dispose) => {
670
+ mount.appendChild(this.render());
671
+ return dispose;
672
+ });
673
+ }
674
+ disconnectedCallback() {
675
+ if (this._dispose === null) {
676
+ return; // not mounted
677
+ }
678
+ this._dispose();
679
+ this._dispose = null;
680
+ const mount = this.getMountPoint();
681
+ while (mount.firstChild) {
682
+ mount.removeChild(mount.firstChild);
683
+ }
684
+ }
685
+ }
686
+ cachedBase = SignalsElementBase;
687
+ return cachedBase;
688
+ }
689
+ // `SignalsElement` is the ergonomic name to `extends`. It is a lazily-resolved alias
690
+ // for the class built by `createSignalsElement()`: a Proxy that forwards construct /
691
+ // get / has / getPrototypeOf to the real (memoized) base, building it on first touch.
692
+ // This keeps `class X extends SignalsElement { … }` working exactly as before WHILE
693
+ // deferring the `HTMLElement` reference to subclass-definition time (in a real DOM),
694
+ // instead of `./dom` module-evaluation time (which may be a non-DOM SSR pre-pass).
695
+ //
696
+ // BREAKING note: the value is now a Proxy, not the class object literal. Subclassing,
697
+ // `instanceof`, and static/prototype access all still work; a consumer that relied on
698
+ // `SignalsElement` being a *frozen* concrete class reference (uncommon) should call
699
+ // `createSignalsElement()` to obtain the real class.
700
+ // The Proxy target is a never-called placeholder (every operation is intercepted by a
701
+ // trap below); it only needs to be constructable/callable so `new` / `extends` and the
702
+ // `apply` trap engage. It is intentionally a no-op and never executed.
703
+ /* v8 ignore next */
704
+ const signalsElementTarget = function () { };
705
+ const SignalsElement = new Proxy(signalsElementTarget, {
706
+ construct(_target, args, newTarget) {
707
+ return Reflect.construct(createSignalsElement(), args, newTarget);
708
+ },
709
+ get(_target, prop, receiver) {
710
+ return Reflect.get(createSignalsElement(), prop, receiver);
711
+ },
712
+ has(_target, prop) {
713
+ return Reflect.has(createSignalsElement(), prop);
714
+ },
715
+ apply() {
716
+ throw new TypeError("SignalsElement is a class and cannot be invoked without `new`/`extends`.");
717
+ },
718
+ getPrototypeOf() {
719
+ return Reflect.getPrototypeOf(createSignalsElement());
720
+ },
721
+ });
722
+ function toNodes(value) {
723
+ const out = [];
724
+ const visit = (v) => {
725
+ if (v == null || typeof v === "boolean") {
726
+ return;
727
+ }
728
+ if (Array.isArray(v)) {
729
+ v.forEach(visit);
730
+ return;
731
+ }
732
+ if (v instanceof Node) {
733
+ out.push(v);
734
+ return;
735
+ }
736
+ out.push(document.createTextNode(String(v)));
737
+ };
738
+ visit(value);
739
+ return out;
740
+ }
741
+
742
+ export { For, Fragment, Index, SignalsElement, createRoot, createSignalsElement, effect, h, onCleanup, render, signal };
743
+ //# sourceMappingURL=dom.esm.js.map