@pyreon/runtime-dom 0.15.0 → 0.18.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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"707cac82-1","name":"delegate.ts"},{"uid":"707cac82-3","name":"hydration-debug.ts"},{"uid":"707cac82-5","name":"devtools.ts"},{"uid":"707cac82-7","name":"nodes.ts"},{"uid":"707cac82-9","name":"props.ts"},{"uid":"707cac82-11","name":"mount.ts"},{"uid":"707cac82-13","name":"hydrate.ts"},{"uid":"707cac82-15","name":"keep-alive.ts"},{"uid":"707cac82-17","name":"template.ts"},{"uid":"707cac82-19","name":"transition.ts"},{"uid":"707cac82-21","name":"transition-group.ts"},{"uid":"707cac82-23","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"707cac82-1":{"renderedLength":2090,"gzipLength":1029,"brotliLength":0,"metaUid":"707cac82-0"},"707cac82-3":{"renderedLength":1395,"gzipLength":718,"brotliLength":0,"metaUid":"707cac82-2"},"707cac82-5":{"renderedLength":7009,"gzipLength":2149,"brotliLength":0,"metaUid":"707cac82-4"},"707cac82-7":{"renderedLength":16850,"gzipLength":4538,"brotliLength":0,"metaUid":"707cac82-6"},"707cac82-9":{"renderedLength":7955,"gzipLength":3028,"brotliLength":0,"metaUid":"707cac82-8"},"707cac82-11":{"renderedLength":12232,"gzipLength":3882,"brotliLength":0,"metaUid":"707cac82-10"},"707cac82-13":{"renderedLength":8312,"gzipLength":2472,"brotliLength":0,"metaUid":"707cac82-12"},"707cac82-15":{"renderedLength":1498,"gzipLength":713,"brotliLength":0,"metaUid":"707cac82-14"},"707cac82-17":{"renderedLength":5802,"gzipLength":2272,"brotliLength":0,"metaUid":"707cac82-16"},"707cac82-19":{"renderedLength":4929,"gzipLength":1410,"brotliLength":0,"metaUid":"707cac82-18"},"707cac82-21":{"renderedLength":7973,"gzipLength":2085,"brotliLength":0,"metaUid":"707cac82-20"},"707cac82-23":{"renderedLength":985,"gzipLength":549,"brotliLength":0,"metaUid":"707cac82-22"}},"nodeMetas":{"707cac82-0":{"id":"/src/delegate.ts","moduleParts":{"index.js":"707cac82-1"},"imported":[{"uid":"707cac82-24"}],"importedBy":[{"uid":"707cac82-22"},{"uid":"707cac82-12"},{"uid":"707cac82-8"}]},"707cac82-2":{"id":"/src/hydration-debug.ts","moduleParts":{"index.js":"707cac82-3"},"imported":[],"importedBy":[{"uid":"707cac82-22"},{"uid":"707cac82-12"}]},"707cac82-4":{"id":"/src/devtools.ts","moduleParts":{"index.js":"707cac82-5"},"imported":[],"importedBy":[{"uid":"707cac82-22"},{"uid":"707cac82-10"}]},"707cac82-6":{"id":"/src/nodes.ts","moduleParts":{"index.js":"707cac82-7"},"imported":[{"uid":"707cac82-25"},{"uid":"707cac82-24"}],"importedBy":[{"uid":"707cac82-12"},{"uid":"707cac82-10"}]},"707cac82-8":{"id":"/src/props.ts","moduleParts":{"index.js":"707cac82-9"},"imported":[{"uid":"707cac82-25"},{"uid":"707cac82-24"},{"uid":"707cac82-0"}],"importedBy":[{"uid":"707cac82-22"},{"uid":"707cac82-12"},{"uid":"707cac82-10"}]},"707cac82-10":{"id":"/src/mount.ts","moduleParts":{"index.js":"707cac82-11"},"imported":[{"uid":"707cac82-25"},{"uid":"707cac82-24"},{"uid":"707cac82-4"},{"uid":"707cac82-6"},{"uid":"707cac82-8"}],"importedBy":[{"uid":"707cac82-22"},{"uid":"707cac82-12"},{"uid":"707cac82-14"},{"uid":"707cac82-16"},{"uid":"707cac82-20"}]},"707cac82-12":{"id":"/src/hydrate.ts","moduleParts":{"index.js":"707cac82-13"},"imported":[{"uid":"707cac82-25"},{"uid":"707cac82-24"},{"uid":"707cac82-0"},{"uid":"707cac82-2"},{"uid":"707cac82-10"},{"uid":"707cac82-6"},{"uid":"707cac82-8"}],"importedBy":[{"uid":"707cac82-22"}]},"707cac82-14":{"id":"/src/keep-alive.ts","moduleParts":{"index.js":"707cac82-15"},"imported":[{"uid":"707cac82-25"},{"uid":"707cac82-24"},{"uid":"707cac82-10"}],"importedBy":[{"uid":"707cac82-22"}]},"707cac82-16":{"id":"/src/template.ts","moduleParts":{"index.js":"707cac82-17"},"imported":[{"uid":"707cac82-24"},{"uid":"707cac82-10"}],"importedBy":[{"uid":"707cac82-22"}]},"707cac82-18":{"id":"/src/transition.ts","moduleParts":{"index.js":"707cac82-19"},"imported":[{"uid":"707cac82-25"},{"uid":"707cac82-24"}],"importedBy":[{"uid":"707cac82-22"}]},"707cac82-20":{"id":"/src/transition-group.ts","moduleParts":{"index.js":"707cac82-21"},"imported":[{"uid":"707cac82-25"},{"uid":"707cac82-24"},{"uid":"707cac82-10"}],"importedBy":[{"uid":"707cac82-22"}]},"707cac82-22":{"id":"/src/index.ts","moduleParts":{"index.js":"707cac82-23"},"imported":[{"uid":"707cac82-0"},{"uid":"707cac82-12"},{"uid":"707cac82-2"},{"uid":"707cac82-14"},{"uid":"707cac82-10"},{"uid":"707cac82-8"},{"uid":"707cac82-16"},{"uid":"707cac82-18"},{"uid":"707cac82-20"},{"uid":"707cac82-4"}],"importedBy":[],"isEntry":true},"707cac82-24":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"707cac82-0"},{"uid":"707cac82-12"},{"uid":"707cac82-14"},{"uid":"707cac82-10"},{"uid":"707cac82-8"},{"uid":"707cac82-16"},{"uid":"707cac82-18"},{"uid":"707cac82-20"},{"uid":"707cac82-6"}]},"707cac82-25":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"707cac82-12"},{"uid":"707cac82-14"},{"uid":"707cac82-10"},{"uid":"707cac82-8"},{"uid":"707cac82-18"},{"uid":"707cac82-20"},{"uid":"707cac82-6"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"8df5ffbc-1","name":"delegate.ts"},{"uid":"8df5ffbc-3","name":"hydration-debug.ts"},{"uid":"8df5ffbc-5","name":"devtools.ts"},{"uid":"8df5ffbc-7","name":"nodes.ts"},{"uid":"8df5ffbc-9","name":"props.ts"},{"uid":"8df5ffbc-11","name":"mount.ts"},{"uid":"8df5ffbc-13","name":"hydrate.ts"},{"uid":"8df5ffbc-15","name":"keep-alive.ts"},{"uid":"8df5ffbc-17","name":"template.ts"},{"uid":"8df5ffbc-19","name":"transition.ts"},{"uid":"8df5ffbc-21","name":"transition-group.ts"},{"uid":"8df5ffbc-23","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"8df5ffbc-1":{"renderedLength":2090,"gzipLength":1029,"brotliLength":0,"metaUid":"8df5ffbc-0"},"8df5ffbc-3":{"renderedLength":1395,"gzipLength":718,"brotliLength":0,"metaUid":"8df5ffbc-2"},"8df5ffbc-5":{"renderedLength":7009,"gzipLength":2149,"brotliLength":0,"metaUid":"8df5ffbc-4"},"8df5ffbc-7":{"renderedLength":17556,"gzipLength":4660,"brotliLength":0,"metaUid":"8df5ffbc-6"},"8df5ffbc-9":{"renderedLength":8288,"gzipLength":3134,"brotliLength":0,"metaUid":"8df5ffbc-8"},"8df5ffbc-11":{"renderedLength":12232,"gzipLength":3882,"brotliLength":0,"metaUid":"8df5ffbc-10"},"8df5ffbc-13":{"renderedLength":8312,"gzipLength":2472,"brotliLength":0,"metaUid":"8df5ffbc-12"},"8df5ffbc-15":{"renderedLength":1518,"gzipLength":724,"brotliLength":0,"metaUid":"8df5ffbc-14"},"8df5ffbc-17":{"renderedLength":5942,"gzipLength":2283,"brotliLength":0,"metaUid":"8df5ffbc-16"},"8df5ffbc-19":{"renderedLength":4929,"gzipLength":1410,"brotliLength":0,"metaUid":"8df5ffbc-18"},"8df5ffbc-21":{"renderedLength":8002,"gzipLength":2092,"brotliLength":0,"metaUid":"8df5ffbc-20"},"8df5ffbc-23":{"renderedLength":985,"gzipLength":549,"brotliLength":0,"metaUid":"8df5ffbc-22"}},"nodeMetas":{"8df5ffbc-0":{"id":"/src/delegate.ts","moduleParts":{"index.js":"8df5ffbc-1"},"imported":[{"uid":"8df5ffbc-24"}],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-8"}]},"8df5ffbc-2":{"id":"/src/hydration-debug.ts","moduleParts":{"index.js":"8df5ffbc-3"},"imported":[],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-12"}]},"8df5ffbc-4":{"id":"/src/devtools.ts","moduleParts":{"index.js":"8df5ffbc-5"},"imported":[],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-10"}]},"8df5ffbc-6":{"id":"/src/nodes.ts","moduleParts":{"index.js":"8df5ffbc-7"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"}],"importedBy":[{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-10"}]},"8df5ffbc-8":{"id":"/src/props.ts","moduleParts":{"index.js":"8df5ffbc-9"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-0"}],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-10"}]},"8df5ffbc-10":{"id":"/src/mount.ts","moduleParts":{"index.js":"8df5ffbc-11"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-4"},{"uid":"8df5ffbc-6"},{"uid":"8df5ffbc-8"}],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-14"},{"uid":"8df5ffbc-16"},{"uid":"8df5ffbc-20"}]},"8df5ffbc-12":{"id":"/src/hydrate.ts","moduleParts":{"index.js":"8df5ffbc-13"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-0"},{"uid":"8df5ffbc-2"},{"uid":"8df5ffbc-10"},{"uid":"8df5ffbc-6"},{"uid":"8df5ffbc-8"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-14":{"id":"/src/keep-alive.ts","moduleParts":{"index.js":"8df5ffbc-15"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-10"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-16":{"id":"/src/template.ts","moduleParts":{"index.js":"8df5ffbc-17"},"imported":[{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-10"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-18":{"id":"/src/transition.ts","moduleParts":{"index.js":"8df5ffbc-19"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-20":{"id":"/src/transition-group.ts","moduleParts":{"index.js":"8df5ffbc-21"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-10"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-22":{"id":"/src/index.ts","moduleParts":{"index.js":"8df5ffbc-23"},"imported":[{"uid":"8df5ffbc-0"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-2"},{"uid":"8df5ffbc-14"},{"uid":"8df5ffbc-10"},{"uid":"8df5ffbc-8"},{"uid":"8df5ffbc-16"},{"uid":"8df5ffbc-18"},{"uid":"8df5ffbc-20"},{"uid":"8df5ffbc-4"}],"importedBy":[],"isEntry":true},"8df5ffbc-24":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"8df5ffbc-0"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-14"},{"uid":"8df5ffbc-10"},{"uid":"8df5ffbc-8"},{"uid":"8df5ffbc-16"},{"uid":"8df5ffbc-18"},{"uid":"8df5ffbc-20"},{"uid":"8df5ffbc-6"}]},"8df5ffbc-25":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-14"},{"uid":"8df5ffbc-10"},{"uid":"8df5ffbc-8"},{"uid":"8df5ffbc-18"},{"uid":"8df5ffbc-20"},{"uid":"8df5ffbc-6"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"keep-alive-entry.js","children":[{"name":"src","children":[{"uid":"5601753e-1","name":"devtools.ts"},{"uid":"5601753e-3","name":"nodes.ts"},{"uid":"5601753e-5","name":"delegate.ts"},{"uid":"5601753e-7","name":"props.ts"},{"uid":"5601753e-9","name":"mount.ts"},{"uid":"5601753e-11","name":"keep-alive.ts"}]}]}],"isRoot":true},"nodeParts":{"5601753e-1":{"renderedLength":759,"gzipLength":340,"brotliLength":0,"metaUid":"5601753e-0"},"5601753e-3":{"renderedLength":16850,"gzipLength":4538,"brotliLength":0,"metaUid":"5601753e-2"},"5601753e-5":{"renderedLength":790,"gzipLength":436,"brotliLength":0,"metaUid":"5601753e-4"},"5601753e-7":{"renderedLength":7485,"gzipLength":2872,"brotliLength":0,"metaUid":"5601753e-6"},"5601753e-9":{"renderedLength":12162,"gzipLength":3873,"brotliLength":0,"metaUid":"5601753e-8"},"5601753e-11":{"renderedLength":1498,"gzipLength":713,"brotliLength":0,"metaUid":"5601753e-10"}},"nodeMetas":{"5601753e-0":{"id":"/src/devtools.ts","moduleParts":{"keep-alive-entry.js":"5601753e-1"},"imported":[],"importedBy":[{"uid":"5601753e-8"}]},"5601753e-2":{"id":"/src/nodes.ts","moduleParts":{"keep-alive-entry.js":"5601753e-3"},"imported":[{"uid":"5601753e-12"},{"uid":"5601753e-13"}],"importedBy":[{"uid":"5601753e-8"}]},"5601753e-4":{"id":"/src/delegate.ts","moduleParts":{"keep-alive-entry.js":"5601753e-5"},"imported":[{"uid":"5601753e-13"}],"importedBy":[{"uid":"5601753e-6"}]},"5601753e-6":{"id":"/src/props.ts","moduleParts":{"keep-alive-entry.js":"5601753e-7"},"imported":[{"uid":"5601753e-12"},{"uid":"5601753e-13"},{"uid":"5601753e-4"}],"importedBy":[{"uid":"5601753e-8"}]},"5601753e-8":{"id":"/src/mount.ts","moduleParts":{"keep-alive-entry.js":"5601753e-9"},"imported":[{"uid":"5601753e-12"},{"uid":"5601753e-13"},{"uid":"5601753e-0"},{"uid":"5601753e-2"},{"uid":"5601753e-6"}],"importedBy":[{"uid":"5601753e-10"}]},"5601753e-10":{"id":"/src/keep-alive.ts","moduleParts":{"keep-alive-entry.js":"5601753e-11"},"imported":[{"uid":"5601753e-12"},{"uid":"5601753e-13"},{"uid":"5601753e-8"}],"importedBy":[],"isEntry":true},"5601753e-12":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"5601753e-10"},{"uid":"5601753e-8"},{"uid":"5601753e-2"},{"uid":"5601753e-6"}]},"5601753e-13":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"5601753e-10"},{"uid":"5601753e-8"},{"uid":"5601753e-2"},{"uid":"5601753e-6"},{"uid":"5601753e-4"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"keep-alive-entry.js","children":[{"name":"src","children":[{"uid":"56ea40e5-1","name":"devtools.ts"},{"uid":"56ea40e5-3","name":"nodes.ts"},{"uid":"56ea40e5-5","name":"delegate.ts"},{"uid":"56ea40e5-7","name":"props.ts"},{"uid":"56ea40e5-9","name":"mount.ts"},{"uid":"56ea40e5-11","name":"keep-alive.ts"}]}]}],"isRoot":true},"nodeParts":{"56ea40e5-1":{"renderedLength":759,"gzipLength":340,"brotliLength":0,"metaUid":"56ea40e5-0"},"56ea40e5-3":{"renderedLength":17556,"gzipLength":4658,"brotliLength":0,"metaUid":"56ea40e5-2"},"56ea40e5-5":{"renderedLength":790,"gzipLength":436,"brotliLength":0,"metaUid":"56ea40e5-4"},"56ea40e5-7":{"renderedLength":7818,"gzipLength":2974,"brotliLength":0,"metaUid":"56ea40e5-6"},"56ea40e5-9":{"renderedLength":12162,"gzipLength":3873,"brotliLength":0,"metaUid":"56ea40e5-8"},"56ea40e5-11":{"renderedLength":1518,"gzipLength":724,"brotliLength":0,"metaUid":"56ea40e5-10"}},"nodeMetas":{"56ea40e5-0":{"id":"/src/devtools.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-1"},"imported":[],"importedBy":[{"uid":"56ea40e5-8"}]},"56ea40e5-2":{"id":"/src/nodes.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-3"},"imported":[{"uid":"56ea40e5-12"},{"uid":"56ea40e5-13"}],"importedBy":[{"uid":"56ea40e5-8"}]},"56ea40e5-4":{"id":"/src/delegate.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-5"},"imported":[{"uid":"56ea40e5-13"}],"importedBy":[{"uid":"56ea40e5-6"}]},"56ea40e5-6":{"id":"/src/props.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-7"},"imported":[{"uid":"56ea40e5-12"},{"uid":"56ea40e5-13"},{"uid":"56ea40e5-4"}],"importedBy":[{"uid":"56ea40e5-8"}]},"56ea40e5-8":{"id":"/src/mount.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-9"},"imported":[{"uid":"56ea40e5-12"},{"uid":"56ea40e5-13"},{"uid":"56ea40e5-0"},{"uid":"56ea40e5-2"},{"uid":"56ea40e5-6"}],"importedBy":[{"uid":"56ea40e5-10"}]},"56ea40e5-10":{"id":"/src/keep-alive.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-11"},"imported":[{"uid":"56ea40e5-12"},{"uid":"56ea40e5-13"},{"uid":"56ea40e5-8"}],"importedBy":[],"isEntry":true},"56ea40e5-12":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"56ea40e5-10"},{"uid":"56ea40e5-8"},{"uid":"56ea40e5-2"},{"uid":"56ea40e5-6"}]},"56ea40e5-13":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"56ea40e5-10"},{"uid":"56ea40e5-8"},{"uid":"56ea40e5-2"},{"uid":"56ea40e5-6"},{"uid":"56ea40e5-4"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -341,7 +341,7 @@ function installDevTools() {
341
341
  //#endregion
342
342
  //#region src/nodes.ts
343
343
  const __DEV__$5 = process.env.NODE_ENV !== "production";
344
- const _countSink$3 = globalThis;
344
+ const _countSink$4 = globalThis;
345
345
  /**
346
346
  * Move all nodes strictly between `start` and `end` into a throwaway
347
347
  * DocumentFragment, detaching them from the live DOM in O(n) top-level moves.
@@ -363,6 +363,10 @@ function clearBetween(start, end) {
363
363
  cur = next;
364
364
  }
365
365
  }
366
+ /** Emit `runtime.cleanup` once per registered mount cleanup that actually runs. */
367
+ function _emitCleanup() {
368
+ if (__DEV__$5) _countSink$4.__pyreon_count__?.("runtime.cleanup");
369
+ }
366
370
  /**
367
371
  * Mount a reactive node whose content changes over time.
368
372
  *
@@ -370,24 +374,34 @@ function clearBetween(start, end) {
370
374
  * On each change: old nodes are removed, new ones inserted before the anchor.
371
375
  */
372
376
  function mountReactive(accessor, parent, anchor, mount) {
377
+ if (__DEV__$5) _countSink$4.__pyreon_count__?.("runtime.mountReactive");
373
378
  const marker = document.createComment("pyreon");
374
379
  parent.insertBefore(marker, anchor);
375
380
  const contextSnapshot = captureContextStack();
376
381
  let currentCleanup = () => {};
382
+ let hasCleanup = false;
377
383
  let generation = 0;
378
384
  const e = effect(() => {
379
385
  const myGen = ++generation;
386
+ if (hasCleanup) _emitCleanup();
380
387
  runUntracked(() => currentCleanup());
381
388
  currentCleanup = () => {};
389
+ hasCleanup = false;
382
390
  const value = accessor();
383
391
  if (value != null && value !== false) {
384
392
  const cleanup = runUntracked(() => restoreContextStack(contextSnapshot, () => mount(value, parent, marker)));
385
- if (myGen === generation) currentCleanup = cleanup;
386
- else cleanup();
393
+ if (myGen === generation) {
394
+ currentCleanup = cleanup;
395
+ hasCleanup = true;
396
+ } else {
397
+ _emitCleanup();
398
+ cleanup();
399
+ }
387
400
  }
388
401
  });
389
402
  return () => {
390
403
  e.dispose();
404
+ if (hasCleanup) _emitCleanup();
391
405
  currentCleanup();
392
406
  marker.parentNode?.removeChild(marker);
393
407
  };
@@ -424,7 +438,7 @@ function computeKeyedLis(lis, n, newKeyOrder, curPos) {
424
438
  if (lo > 0) pred[i] = tailIdx[lo - 1];
425
439
  if (lo === lisLen) lisLen++;
426
440
  }
427
- if (__DEV__$5 && ops > 0) _countSink$3.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
441
+ if (__DEV__$5 && ops > 0) _countSink$4.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
428
442
  return lisLen;
429
443
  }
430
444
  function markStayingEntries(lis, lisLen) {
@@ -487,6 +501,7 @@ function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
487
501
  const removeStaleEntries = (newKeySet) => {
488
502
  for (const [key, entry] of cache) {
489
503
  if (newKeySet.has(key)) continue;
504
+ _emitCleanup();
490
505
  entry.cleanup();
491
506
  entry.anchor.parentNode?.removeChild(entry.anchor);
492
507
  cache.delete(key);
@@ -511,28 +526,34 @@ function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
511
526
  const e = effect(() => {
512
527
  const newList = accessor();
513
528
  const n = newList.length;
514
- if (n === 0 && cache.size > 0) {
515
- for (const entry of cache.values()) entry.cleanup();
516
- cache.clear();
529
+ runUntracked(() => {
530
+ if (n === 0 && cache.size > 0) {
531
+ for (const entry of cache.values()) {
532
+ _emitCleanup();
533
+ entry.cleanup();
534
+ }
535
+ cache.clear();
536
+ curPos.clear();
537
+ currentKeyOrder = [];
538
+ clearBetween(startMarker, tailMarker);
539
+ return;
540
+ }
541
+ const { newKeyOrder, newKeySet } = collectKeyOrder(newList);
542
+ removeStaleEntries(newKeySet);
543
+ mountNewEntries(newList);
544
+ if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker);
517
545
  curPos.clear();
518
- currentKeyOrder = [];
519
- clearBetween(startMarker, tailMarker);
520
- return;
521
- }
522
- const { newKeyOrder, newKeySet } = collectKeyOrder(newList);
523
- removeStaleEntries(newKeySet);
524
- mountNewEntries(newList);
525
- if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker);
526
- curPos.clear();
527
- for (let i = 0; i < newKeyOrder.length; i++) {
528
- const k = newKeyOrder[i];
529
- if (k !== void 0) curPos.set(k, i);
530
- }
531
- currentKeyOrder = newKeyOrder;
546
+ for (let i = 0; i < newKeyOrder.length; i++) {
547
+ const k = newKeyOrder[i];
548
+ if (k !== void 0) curPos.set(k, i);
549
+ }
550
+ currentKeyOrder = newKeyOrder;
551
+ });
532
552
  });
533
553
  return () => {
534
554
  e.dispose();
535
555
  for (const entry of cache.values()) {
556
+ _emitCleanup();
536
557
  entry.cleanup();
537
558
  entry.anchor.parentNode?.removeChild(entry.anchor);
538
559
  }
@@ -591,7 +612,7 @@ function computeForLis(lis, n, newKeys, cache) {
591
612
  tailIdx[lo] = i;
592
613
  if (lo > 0) pred[i] = tailIdx[lo - 1];
593
614
  }
594
- if (__DEV__$5 && ops > 0) _countSink$3.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
615
+ if (__DEV__$5 && ops > 0) _countSink$4.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
595
616
  return lisLen;
596
617
  }
597
618
  function applyForMoves(n, newKeys, stay, cache, liveParent, tailMarker) {
@@ -709,7 +730,10 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
709
730
  };
710
731
  const handleReplaceAll = (items, n, newKeys, liveParent) => {
711
732
  if (cleanupCount > 0) {
712
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
733
+ for (const entry of cache.values()) if (entry.cleanup) {
734
+ _emitCleanup();
735
+ entry.cleanup();
736
+ }
713
737
  }
714
738
  cache = /* @__PURE__ */ new Map();
715
739
  cleanupCount = 0;
@@ -734,6 +758,7 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
734
758
  for (const [key, entry] of cache) {
735
759
  if (newKeySet.has(key)) continue;
736
760
  if (entry.cleanup) {
761
+ _emitCleanup();
737
762
  entry.cleanup();
738
763
  cleanupCount--;
739
764
  }
@@ -753,7 +778,10 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
753
778
  const handleFastClear = (liveParent) => {
754
779
  if (cache.size === 0) return;
755
780
  if (cleanupCount > 0) {
756
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
781
+ for (const entry of cache.values()) if (entry.cleanup) {
782
+ _emitCleanup();
783
+ entry.cleanup();
784
+ }
757
785
  }
758
786
  const pp = liveParent.parentNode;
759
787
  if (pp && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker) {
@@ -791,25 +819,30 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
791
819
  if (!liveParent) return;
792
820
  const items = source();
793
821
  const n = items.length;
794
- if (n === 0) {
795
- handleFastClear(liveParent);
796
- return;
797
- }
798
- if (currentKeys.length === 0) {
799
- handleFreshRender(items, n, liveParent);
800
- return;
801
- }
802
- const newKeys = collectNewKeys(items, n);
803
- if (!hasAnyKeptKey(n, newKeys)) {
804
- handleReplaceAll(items, n, newKeys, liveParent);
805
- return;
806
- }
807
- handleIncrementalUpdate(items, n, newKeys, liveParent);
822
+ runUntracked(() => {
823
+ if (n === 0) {
824
+ handleFastClear(liveParent);
825
+ return;
826
+ }
827
+ if (currentKeys.length === 0) {
828
+ handleFreshRender(items, n, liveParent);
829
+ return;
830
+ }
831
+ const newKeys = collectNewKeys(items, n);
832
+ if (!hasAnyKeptKey(n, newKeys)) {
833
+ handleReplaceAll(items, n, newKeys, liveParent);
834
+ return;
835
+ }
836
+ handleIncrementalUpdate(items, n, newKeys, liveParent);
837
+ });
808
838
  });
809
839
  return () => {
810
840
  e.dispose();
811
841
  for (const entry of cache.values()) {
812
- if (cleanupCount > 0 && entry.cleanup) entry.cleanup();
842
+ if (cleanupCount > 0 && entry.cleanup) {
843
+ _emitCleanup();
844
+ entry.cleanup();
845
+ }
813
846
  entry.anchor.parentNode?.removeChild(entry.anchor);
814
847
  }
815
848
  cache = /* @__PURE__ */ new Map();
@@ -873,6 +906,7 @@ function moveEntryBefore(parent, startNode, before) {
873
906
  //#endregion
874
907
  //#region src/props.ts
875
908
  const __DEV__$4 = process.env.NODE_ENV !== "production";
909
+ const _countSink$3 = globalThis;
876
910
  let _customSanitizer = null;
877
911
  /**
878
912
  * Set a custom HTML sanitizer used by `innerHTML` and `sanitizeHtml()`.
@@ -1015,7 +1049,10 @@ function applyProps(el, props) {
1015
1049
  let cleanups = null;
1016
1050
  for (const key in props) {
1017
1051
  if (key === "key" || key === "ref" || key === "children") continue;
1018
- const c = applyProp(el, key, props[key]);
1052
+ const descriptor = Object.getOwnPropertyDescriptor(props, key);
1053
+ let c;
1054
+ if (descriptor?.get) c = renderEffect(() => applyStaticProp(el, key, props[key]));
1055
+ else c = applyProp(el, key, props[key]);
1019
1056
  if (c) if (!first) first = c;
1020
1057
  else if (!cleanups) cleanups = [first, c];
1021
1058
  else cleanups.push(c);
@@ -1036,6 +1073,7 @@ function applyProps(el, props) {
1036
1073
  * Bind an event handler (onClick → "click") with batching + delegation support.
1037
1074
  */
1038
1075
  function applyEventProp(el, key, value) {
1076
+ if (__DEV__$4) _countSink$3.__pyreon_count__?.("runtime.applyEvent");
1039
1077
  if (typeof value !== "function") {
1040
1078
  if (__DEV__$4 && value != null) console.warn(`[Pyreon] Event handler "${key}" received a non-function value (${typeof value}). Expected a function. Did you mean ${key}={() => ...}?`);
1041
1079
  return null;
@@ -1082,6 +1120,7 @@ function applyStaticProp(el, key, value) {
1082
1120
  setStaticProp(el, key, value);
1083
1121
  }
1084
1122
  function applyProp(el, key, value) {
1123
+ if (__DEV__$4) _countSink$3.__pyreon_count__?.("runtime.applyProp");
1085
1124
  if (EVENT_RE.test(key)) return applyEventProp(el, key, value);
1086
1125
  if (typeof value === "function") return renderEffect(() => applyStaticProp(el, key, value()));
1087
1126
  applyStaticProp(el, key, value);
@@ -1829,7 +1868,7 @@ function KeepAlive(props) {
1829
1868
  const e = effect(() => {
1830
1869
  const isActive = props.active?.() ?? true;
1831
1870
  if (!childMounted) {
1832
- childCleanup = mountChild(props.children ?? null, container, null);
1871
+ childCleanup = runUntracked(() => mountChild(props.children ?? null, container, null));
1833
1872
  childMounted = true;
1834
1873
  }
1835
1874
  container.style.display = isActive ? "" : "none";
@@ -1905,6 +1944,7 @@ function createTemplate(html, bind) {
1905
1944
  * @param node - The Text node to update
1906
1945
  */
1907
1946
  function _bindText(source, node) {
1947
+ if (__DEV__$2) _countSink$1.__pyreon_count__?.("runtime.bindText");
1908
1948
  if (source.direct) {
1909
1949
  const textUpdate = () => {
1910
1950
  const v = source._v;
@@ -1938,6 +1978,7 @@ function _bindText(source, node) {
1938
1978
  * @param updater - Function that reads `source._v` and applies the DOM update
1939
1979
  */
1940
1980
  function _bindDirect(source, updater) {
1981
+ if (__DEV__$2) _countSink$1.__pyreon_count__?.("runtime.bindDirect");
1941
1982
  if (source.direct) {
1942
1983
  updater(source._v);
1943
1984
  return source.direct(() => updater(source._v));
@@ -2312,17 +2353,19 @@ function TransitionGroup(props) {
2312
2353
  const key = props.keyFn(item, i);
2313
2354
  if (entries.has(key)) continue;
2314
2355
  const itemRef = createRef();
2315
- const rawVNode = runUntracked(() => props.render(item, i));
2316
2356
  const entry = {
2317
2357
  key,
2318
2358
  ref: itemRef,
2319
- cleanup: mountChild(typeof rawVNode.type === "string" ? {
2320
- ...rawVNode,
2321
- props: {
2322
- ...rawVNode.props,
2323
- ref: itemRef
2324
- }
2325
- } : rawVNode, container, null),
2359
+ cleanup: runUntracked(() => {
2360
+ const rawVNode = props.render(item, i);
2361
+ return mountChild(typeof rawVNode.type === "string" ? {
2362
+ ...rawVNode,
2363
+ props: {
2364
+ ...rawVNode.props,
2365
+ ref: itemRef
2366
+ }
2367
+ } : rawVNode, container, null);
2368
+ }),
2326
2369
  leaving: false,
2327
2370
  cancelTransition: null
2328
2371
  };
@@ -34,7 +34,7 @@ function unregisterComponent(id) {
34
34
  //#endregion
35
35
  //#region src/nodes.ts
36
36
  const __DEV__$2 = process.env.NODE_ENV !== "production";
37
- const _countSink$1 = globalThis;
37
+ const _countSink$2 = globalThis;
38
38
  /**
39
39
  * Move all nodes strictly between `start` and `end` into a throwaway
40
40
  * DocumentFragment, detaching them from the live DOM in O(n) top-level moves.
@@ -56,6 +56,10 @@ function clearBetween(start, end) {
56
56
  cur = next;
57
57
  }
58
58
  }
59
+ /** Emit `runtime.cleanup` once per registered mount cleanup that actually runs. */
60
+ function _emitCleanup() {
61
+ if (__DEV__$2) _countSink$2.__pyreon_count__?.("runtime.cleanup");
62
+ }
59
63
  /**
60
64
  * Mount a reactive node whose content changes over time.
61
65
  *
@@ -63,24 +67,34 @@ function clearBetween(start, end) {
63
67
  * On each change: old nodes are removed, new ones inserted before the anchor.
64
68
  */
65
69
  function mountReactive(accessor, parent, anchor, mount) {
70
+ if (__DEV__$2) _countSink$2.__pyreon_count__?.("runtime.mountReactive");
66
71
  const marker = document.createComment("pyreon");
67
72
  parent.insertBefore(marker, anchor);
68
73
  const contextSnapshot = captureContextStack();
69
74
  let currentCleanup = () => {};
75
+ let hasCleanup = false;
70
76
  let generation = 0;
71
77
  const e = effect(() => {
72
78
  const myGen = ++generation;
79
+ if (hasCleanup) _emitCleanup();
73
80
  runUntracked(() => currentCleanup());
74
81
  currentCleanup = () => {};
82
+ hasCleanup = false;
75
83
  const value = accessor();
76
84
  if (value != null && value !== false) {
77
85
  const cleanup = runUntracked(() => restoreContextStack(contextSnapshot, () => mount(value, parent, marker)));
78
- if (myGen === generation) currentCleanup = cleanup;
79
- else cleanup();
86
+ if (myGen === generation) {
87
+ currentCleanup = cleanup;
88
+ hasCleanup = true;
89
+ } else {
90
+ _emitCleanup();
91
+ cleanup();
92
+ }
80
93
  }
81
94
  });
82
95
  return () => {
83
96
  e.dispose();
97
+ if (hasCleanup) _emitCleanup();
84
98
  currentCleanup();
85
99
  marker.parentNode?.removeChild(marker);
86
100
  };
@@ -117,7 +131,7 @@ function computeKeyedLis(lis, n, newKeyOrder, curPos) {
117
131
  if (lo > 0) pred[i] = tailIdx[lo - 1];
118
132
  if (lo === lisLen) lisLen++;
119
133
  }
120
- if (__DEV__$2 && ops > 0) _countSink$1.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
134
+ if (__DEV__$2 && ops > 0) _countSink$2.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
121
135
  return lisLen;
122
136
  }
123
137
  function markStayingEntries(lis, lisLen) {
@@ -180,6 +194,7 @@ function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
180
194
  const removeStaleEntries = (newKeySet) => {
181
195
  for (const [key, entry] of cache) {
182
196
  if (newKeySet.has(key)) continue;
197
+ _emitCleanup();
183
198
  entry.cleanup();
184
199
  entry.anchor.parentNode?.removeChild(entry.anchor);
185
200
  cache.delete(key);
@@ -204,28 +219,34 @@ function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
204
219
  const e = effect(() => {
205
220
  const newList = accessor();
206
221
  const n = newList.length;
207
- if (n === 0 && cache.size > 0) {
208
- for (const entry of cache.values()) entry.cleanup();
209
- cache.clear();
222
+ runUntracked(() => {
223
+ if (n === 0 && cache.size > 0) {
224
+ for (const entry of cache.values()) {
225
+ _emitCleanup();
226
+ entry.cleanup();
227
+ }
228
+ cache.clear();
229
+ curPos.clear();
230
+ currentKeyOrder = [];
231
+ clearBetween(startMarker, tailMarker);
232
+ return;
233
+ }
234
+ const { newKeyOrder, newKeySet } = collectKeyOrder(newList);
235
+ removeStaleEntries(newKeySet);
236
+ mountNewEntries(newList);
237
+ if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker);
210
238
  curPos.clear();
211
- currentKeyOrder = [];
212
- clearBetween(startMarker, tailMarker);
213
- return;
214
- }
215
- const { newKeyOrder, newKeySet } = collectKeyOrder(newList);
216
- removeStaleEntries(newKeySet);
217
- mountNewEntries(newList);
218
- if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker);
219
- curPos.clear();
220
- for (let i = 0; i < newKeyOrder.length; i++) {
221
- const k = newKeyOrder[i];
222
- if (k !== void 0) curPos.set(k, i);
223
- }
224
- currentKeyOrder = newKeyOrder;
239
+ for (let i = 0; i < newKeyOrder.length; i++) {
240
+ const k = newKeyOrder[i];
241
+ if (k !== void 0) curPos.set(k, i);
242
+ }
243
+ currentKeyOrder = newKeyOrder;
244
+ });
225
245
  });
226
246
  return () => {
227
247
  e.dispose();
228
248
  for (const entry of cache.values()) {
249
+ _emitCleanup();
229
250
  entry.cleanup();
230
251
  entry.anchor.parentNode?.removeChild(entry.anchor);
231
252
  }
@@ -284,7 +305,7 @@ function computeForLis(lis, n, newKeys, cache) {
284
305
  tailIdx[lo] = i;
285
306
  if (lo > 0) pred[i] = tailIdx[lo - 1];
286
307
  }
287
- if (__DEV__$2 && ops > 0) _countSink$1.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
308
+ if (__DEV__$2 && ops > 0) _countSink$2.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
288
309
  return lisLen;
289
310
  }
290
311
  function applyForMoves(n, newKeys, stay, cache, liveParent, tailMarker) {
@@ -402,7 +423,10 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
402
423
  };
403
424
  const handleReplaceAll = (items, n, newKeys, liveParent) => {
404
425
  if (cleanupCount > 0) {
405
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
426
+ for (const entry of cache.values()) if (entry.cleanup) {
427
+ _emitCleanup();
428
+ entry.cleanup();
429
+ }
406
430
  }
407
431
  cache = /* @__PURE__ */ new Map();
408
432
  cleanupCount = 0;
@@ -427,6 +451,7 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
427
451
  for (const [key, entry] of cache) {
428
452
  if (newKeySet.has(key)) continue;
429
453
  if (entry.cleanup) {
454
+ _emitCleanup();
430
455
  entry.cleanup();
431
456
  cleanupCount--;
432
457
  }
@@ -446,7 +471,10 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
446
471
  const handleFastClear = (liveParent) => {
447
472
  if (cache.size === 0) return;
448
473
  if (cleanupCount > 0) {
449
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
474
+ for (const entry of cache.values()) if (entry.cleanup) {
475
+ _emitCleanup();
476
+ entry.cleanup();
477
+ }
450
478
  }
451
479
  const pp = liveParent.parentNode;
452
480
  if (pp && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker) {
@@ -484,25 +512,30 @@ function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
484
512
  if (!liveParent) return;
485
513
  const items = source();
486
514
  const n = items.length;
487
- if (n === 0) {
488
- handleFastClear(liveParent);
489
- return;
490
- }
491
- if (currentKeys.length === 0) {
492
- handleFreshRender(items, n, liveParent);
493
- return;
494
- }
495
- const newKeys = collectNewKeys(items, n);
496
- if (!hasAnyKeptKey(n, newKeys)) {
497
- handleReplaceAll(items, n, newKeys, liveParent);
498
- return;
499
- }
500
- handleIncrementalUpdate(items, n, newKeys, liveParent);
515
+ runUntracked(() => {
516
+ if (n === 0) {
517
+ handleFastClear(liveParent);
518
+ return;
519
+ }
520
+ if (currentKeys.length === 0) {
521
+ handleFreshRender(items, n, liveParent);
522
+ return;
523
+ }
524
+ const newKeys = collectNewKeys(items, n);
525
+ if (!hasAnyKeptKey(n, newKeys)) {
526
+ handleReplaceAll(items, n, newKeys, liveParent);
527
+ return;
528
+ }
529
+ handleIncrementalUpdate(items, n, newKeys, liveParent);
530
+ });
501
531
  });
502
532
  return () => {
503
533
  e.dispose();
504
534
  for (const entry of cache.values()) {
505
- if (cleanupCount > 0 && entry.cleanup) entry.cleanup();
535
+ if (cleanupCount > 0 && entry.cleanup) {
536
+ _emitCleanup();
537
+ entry.cleanup();
538
+ }
506
539
  entry.anchor.parentNode?.removeChild(entry.anchor);
507
540
  }
508
541
  cache = /* @__PURE__ */ new Map();
@@ -606,6 +639,7 @@ function delegatedPropName(eventName) {
606
639
  //#endregion
607
640
  //#region src/props.ts
608
641
  const __DEV__$1 = process.env.NODE_ENV !== "production";
642
+ const _countSink$1 = globalThis;
609
643
  let _customSanitizer = null;
610
644
  const SAFE_TAGS = new Set([
611
645
  "a",
@@ -729,7 +763,10 @@ function applyProps(el, props) {
729
763
  let cleanups = null;
730
764
  for (const key in props) {
731
765
  if (key === "key" || key === "ref" || key === "children") continue;
732
- const c = applyProp(el, key, props[key]);
766
+ const descriptor = Object.getOwnPropertyDescriptor(props, key);
767
+ let c;
768
+ if (descriptor?.get) c = renderEffect(() => applyStaticProp(el, key, props[key]));
769
+ else c = applyProp(el, key, props[key]);
733
770
  if (c) if (!first) first = c;
734
771
  else if (!cleanups) cleanups = [first, c];
735
772
  else cleanups.push(c);
@@ -750,6 +787,7 @@ function applyProps(el, props) {
750
787
  * Bind an event handler (onClick → "click") with batching + delegation support.
751
788
  */
752
789
  function applyEventProp(el, key, value) {
790
+ if (__DEV__$1) _countSink$1.__pyreon_count__?.("runtime.applyEvent");
753
791
  if (typeof value !== "function") {
754
792
  if (__DEV__$1 && value != null) console.warn(`[Pyreon] Event handler "${key}" received a non-function value (${typeof value}). Expected a function. Did you mean ${key}={() => ...}?`);
755
793
  return null;
@@ -796,6 +834,7 @@ function applyStaticProp(el, key, value) {
796
834
  setStaticProp(el, key, value);
797
835
  }
798
836
  function applyProp(el, key, value) {
837
+ if (__DEV__$1) _countSink$1.__pyreon_count__?.("runtime.applyProp");
799
838
  if (EVENT_RE.test(key)) return applyEventProp(el, key, value);
800
839
  if (typeof value === "function") return renderEffect(() => applyStaticProp(el, key, value()));
801
840
  applyStaticProp(el, key, value);
@@ -1320,7 +1359,7 @@ function KeepAlive(props) {
1320
1359
  const e = effect(() => {
1321
1360
  const isActive = props.active?.() ?? true;
1322
1361
  if (!childMounted) {
1323
- childCleanup = mountChild(props.children ?? null, container, null);
1362
+ childCleanup = runUntracked(() => mountChild(props.children ?? null, container, null));
1324
1363
  childMounted = true;
1325
1364
  }
1326
1365
  container.style.display = isActive ? "" : "none";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/runtime-dom",
3
- "version": "0.15.0",
3
+ "version": "0.18.0",
4
4
  "description": "DOM renderer for Pyreon",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/runtime-dom#readme",
6
6
  "bugs": {
@@ -54,15 +54,15 @@
54
54
  "prepublishOnly": "bun run build"
55
55
  },
56
56
  "dependencies": {
57
- "@pyreon/core": "^0.15.0",
58
- "@pyreon/reactivity": "^0.15.0"
57
+ "@pyreon/core": "^0.18.0",
58
+ "@pyreon/reactivity": "^0.18.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@happy-dom/global-registrator": "^20.8.9",
62
- "@pyreon/compiler": "^0.15.0",
62
+ "@pyreon/compiler": "^0.18.0",
63
63
  "@pyreon/manifest": "0.13.1",
64
- "@pyreon/runtime-server": "^0.15.0",
65
- "@pyreon/test-utils": "^0.13.2",
64
+ "@pyreon/runtime-server": "^0.18.0",
65
+ "@pyreon/test-utils": "^0.13.5",
66
66
  "@vitest/browser-playwright": "^4.1.4",
67
67
  "esbuild": "^0.28.0",
68
68
  "happy-dom": "^20.8.3",
package/src/keep-alive.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Props, VNodeChild } from '@pyreon/core'
2
2
  import { createRef, h, nativeCompat, onMount } from '@pyreon/core'
3
- import { effect } from '@pyreon/reactivity'
3
+ import { effect, runUntracked } from '@pyreon/reactivity'
4
4
  import { mountChild } from './mount'
5
5
 
6
6
  export interface KeepAliveProps extends Props {
@@ -51,8 +51,15 @@ export function KeepAlive(props: KeepAliveProps): VNodeChild {
51
51
  const isActive = props.active?.() ?? true
52
52
 
53
53
  if (!childMounted) {
54
- // Mount children into the container div exactly once
55
- childCleanup = mountChild(props.children ?? null, container, null)
54
+ // Mount children UNTRACKED child component setup must not
55
+ // subscribe this effect. Otherwise an unrelated signal flip in
56
+ // the children would re-run KeepAlive's effect, runCleanup()
57
+ // would dispose the children's inner effects (because they were
58
+ // collected as inner effects of this run via _innerEffectCollector),
59
+ // and the `if (!childMounted)` guard would skip re-mount → the
60
+ // children become permanently un-reactive while still rendered.
61
+ // Same shape as the mountFor / mountKeyedList fix in nodes.ts.
62
+ childCleanup = runUntracked(() => mountChild(props.children ?? null, container, null))
56
63
  childMounted = true
57
64
  }
58
65
 
package/src/nodes.ts CHANGED
@@ -37,6 +37,11 @@ function clearBetween(start: Node, end: Node): void {
37
37
  // frag goes out of scope → nodes are GC-eligible
38
38
  }
39
39
 
40
+ /** Emit `runtime.cleanup` once per registered mount cleanup that actually runs. */
41
+ function _emitCleanup(): void {
42
+ if (__DEV__) _countSink.__pyreon_count__?.('runtime.cleanup')
43
+ }
44
+
40
45
  /**
41
46
  * Mount a reactive node whose content changes over time.
42
47
  *
@@ -49,6 +54,7 @@ export function mountReactive(
49
54
  anchor: Node | null,
50
55
  mount: (child: VNodeChild, p: Node, a: Node | null) => Cleanup,
51
56
  ): Cleanup {
57
+ if (__DEV__) _countSink.__pyreon_count__?.('runtime.mountReactive')
52
58
  const marker = document.createComment('pyreon')
53
59
  parent.insertBefore(marker, anchor)
54
60
 
@@ -61,6 +67,9 @@ export function mountReactive(
61
67
  let currentCleanup: Cleanup = () => {
62
68
  /* noop */
63
69
  }
70
+ // hasCleanup gates `runtime.cleanup` so we don't count the placeholder
71
+ // noop on the first effect run as a "cleanup invocation".
72
+ let hasCleanup = false
64
73
  let generation = 0
65
74
 
66
75
  const e = effect(() => {
@@ -68,10 +77,12 @@ export function mountReactive(
68
77
  // Run cleanup outside tracking context — cleanup may write to signals
69
78
  // (e.g. onUnmount hooks), and those writes must not accidentally register
70
79
  // as dependencies of this effect, which would cause infinite recursion.
80
+ if (hasCleanup) _emitCleanup()
71
81
  runUntracked(() => currentCleanup())
72
82
  currentCleanup = () => {
73
83
  /* noop */
74
84
  }
85
+ hasCleanup = false
75
86
  const value = accessor()
76
87
  // Note: typeof value === 'function' is a VALID return from a reactive
77
88
  // accessor — it represents a nested `() => VNodeChild` accessor (the
@@ -98,7 +109,9 @@ export function mountReactive(
98
109
  // set by the re-entrant run.
99
110
  if (myGen === generation) {
100
111
  currentCleanup = cleanup
112
+ hasCleanup = true
101
113
  } else {
114
+ _emitCleanup()
102
115
  cleanup()
103
116
  }
104
117
  }
@@ -106,6 +119,7 @@ export function mountReactive(
106
119
 
107
120
  return () => {
108
121
  e.dispose()
122
+ if (hasCleanup) _emitCleanup()
109
123
  currentCleanup()
110
124
  marker.parentNode?.removeChild(marker)
111
125
  }
@@ -270,6 +284,7 @@ export function mountKeyedList(
270
284
  const removeStaleEntries = (newKeySet: Set<string | number>) => {
271
285
  for (const [key, entry] of cache) {
272
286
  if (newKeySet.has(key)) continue
287
+ _emitCleanup()
273
288
  entry.cleanup()
274
289
  entry.anchor.parentNode?.removeChild(entry.anchor)
275
290
  cache.delete(key)
@@ -293,35 +308,42 @@ export function mountKeyedList(
293
308
  const e = effect(() => {
294
309
  const newList = accessor()
295
310
  const n = newList.length
311
+ // Same untracking rationale as mountFor — see comment there. Child
312
+ // mounts via mountVNode must not re-track on this effect's run.
313
+ runUntracked(() => {
314
+ if (n === 0 && cache.size > 0) {
315
+ for (const entry of cache.values()) {
316
+ _emitCleanup()
317
+ entry.cleanup()
318
+ }
319
+ cache.clear()
320
+ curPos.clear()
321
+ currentKeyOrder = []
322
+ clearBetween(startMarker, tailMarker)
323
+ return
324
+ }
296
325
 
297
- if (n === 0 && cache.size > 0) {
298
- for (const entry of cache.values()) entry.cleanup()
299
- cache.clear()
300
- curPos.clear()
301
- currentKeyOrder = []
302
- clearBetween(startMarker, tailMarker)
303
- return
304
- }
305
-
306
- const { newKeyOrder, newKeySet } = collectKeyOrder(newList)
307
- removeStaleEntries(newKeySet)
308
- mountNewEntries(newList)
326
+ const { newKeyOrder, newKeySet } = collectKeyOrder(newList)
327
+ removeStaleEntries(newKeySet)
328
+ mountNewEntries(newList)
309
329
 
310
- if (currentKeyOrder.length > 0 && n > 0) {
311
- lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker)
312
- }
330
+ if (currentKeyOrder.length > 0 && n > 0) {
331
+ lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker)
332
+ }
313
333
 
314
- curPos.clear()
315
- for (let i = 0; i < newKeyOrder.length; i++) {
316
- const k = newKeyOrder[i]
317
- if (k !== undefined) curPos.set(k, i)
318
- }
319
- currentKeyOrder = newKeyOrder
334
+ curPos.clear()
335
+ for (let i = 0; i < newKeyOrder.length; i++) {
336
+ const k = newKeyOrder[i]
337
+ if (k !== undefined) curPos.set(k, i)
338
+ }
339
+ currentKeyOrder = newKeyOrder
340
+ })
320
341
  })
321
342
 
322
343
  return () => {
323
344
  e.dispose()
324
345
  for (const entry of cache.values()) {
346
+ _emitCleanup()
325
347
  entry.cleanup()
326
348
  entry.anchor.parentNode?.removeChild(entry.anchor)
327
349
  }
@@ -602,7 +624,12 @@ export function mountFor<T>(
602
624
  liveParent: Node,
603
625
  ) => {
604
626
  if (cleanupCount > 0) {
605
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup()
627
+ for (const entry of cache.values()) {
628
+ if (entry.cleanup) {
629
+ _emitCleanup()
630
+ entry.cleanup()
631
+ }
632
+ }
606
633
  }
607
634
  cache = new Map()
608
635
  cleanupCount = 0
@@ -634,6 +661,7 @@ export function mountFor<T>(
634
661
  for (const [key, entry] of cache) {
635
662
  if (newKeySet.has(key)) continue
636
663
  if (entry.cleanup) {
664
+ _emitCleanup()
637
665
  entry.cleanup()
638
666
  cleanupCount--
639
667
  }
@@ -660,7 +688,12 @@ export function mountFor<T>(
660
688
  const handleFastClear = (liveParent: Node) => {
661
689
  if (cache.size === 0) return
662
690
  if (cleanupCount > 0) {
663
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup()
691
+ for (const entry of cache.values()) {
692
+ if (entry.cleanup) {
693
+ _emitCleanup()
694
+ entry.cleanup()
695
+ }
696
+ }
664
697
  }
665
698
  const pp = liveParent.parentNode
666
699
  if (pp && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker) {
@@ -715,31 +748,45 @@ export function mountFor<T>(
715
748
  if (!liveParent) return
716
749
  const items = source()
717
750
  const n = items.length
751
+ // Child mounts (renderInto → mountChild) must NOT re-track on this
752
+ // effect's run, mirroring mountReactive's pattern at line ~92. Without
753
+ // this, any signal read during a child component's setup (e.g. useQuery
754
+ // calling `new QueryObserver(client, options())` at construction time,
755
+ // which reads any signals inside the options builder) leaks its
756
+ // subscription up to the For effect. A flip of the unrelated signal
757
+ // re-runs For, runCleanup() disposes ALL inner effects, and
758
+ // handleIncrementalUpdate skips re-mount on key match — leaving the
759
+ // subtree's inner effects gone forever. Reproduced by the
760
+ // `<For>`-shaped test in fanout-repro.test.tsx; deferred from PR #490.
761
+ runUntracked(() => {
762
+ if (n === 0) {
763
+ handleFastClear(liveParent)
764
+ return
765
+ }
718
766
 
719
- if (n === 0) {
720
- handleFastClear(liveParent)
721
- return
722
- }
723
-
724
- if (currentKeys.length === 0) {
725
- handleFreshRender(items, n, liveParent)
726
- return
727
- }
767
+ if (currentKeys.length === 0) {
768
+ handleFreshRender(items, n, liveParent)
769
+ return
770
+ }
728
771
 
729
- const newKeys = collectNewKeys(items, n)
772
+ const newKeys = collectNewKeys(items, n)
730
773
 
731
- if (!hasAnyKeptKey(n, newKeys)) {
732
- handleReplaceAll(items, n, newKeys, liveParent)
733
- return
734
- }
774
+ if (!hasAnyKeptKey(n, newKeys)) {
775
+ handleReplaceAll(items, n, newKeys, liveParent)
776
+ return
777
+ }
735
778
 
736
- handleIncrementalUpdate(items, n, newKeys, liveParent)
779
+ handleIncrementalUpdate(items, n, newKeys, liveParent)
780
+ })
737
781
  })
738
782
 
739
783
  return () => {
740
784
  e.dispose()
741
785
  for (const entry of cache.values()) {
742
- if (cleanupCount > 0 && entry.cleanup) entry.cleanup()
786
+ if (cleanupCount > 0 && entry.cleanup) {
787
+ _emitCleanup()
788
+ entry.cleanup()
789
+ }
743
790
  entry.anchor.parentNode?.removeChild(entry.anchor)
744
791
  }
745
792
  cache = new Map()
package/src/props.ts CHANGED
@@ -10,6 +10,9 @@ type Cleanup = () => void
10
10
  // uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
11
11
  const __DEV__ = process.env.NODE_ENV !== 'production'
12
12
 
13
+ // Dev-time counter sink — see packages/internals/perf-harness for contract.
14
+ const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
15
+
13
16
  // ─── Configurable sanitizer ──────────────────────────────────────────────────
14
17
 
15
18
  export type SanitizeFn = (html: string) => string
@@ -176,7 +179,23 @@ export function applyProps(el: Element, props: Props): Cleanup | null {
176
179
  let cleanups: Cleanup[] | null = null
177
180
  for (const key in props) {
178
181
  if (key === 'key' || key === 'ref' || key === 'children') continue
179
- const c = applyProp(el, key, props[key])
182
+ // Getter-shaped descriptors are produced by `makeReactiveProps` from
183
+ // compiler-emitted `_rp(() => signal())` wrappers. A plain
184
+ // `props[key]` read fires the getter once at mount time and stores
185
+ // the resolved value — breaking signal-driven reactivity. Detecting
186
+ // the descriptor and wrapping the read in `renderEffect` here is
187
+ // equivalent to applyProp's existing function-value branch (line 322),
188
+ // routed through the descriptor instead of the value. Other prop
189
+ // pipelines (`splitProps`, `mergeProps`, rocketstyle's
190
+ // descriptor-preserving merges) keep the getter intact end-to-end;
191
+ // this is the final consumer that closes the loop.
192
+ const descriptor = Object.getOwnPropertyDescriptor(props, key)
193
+ let c: Cleanup | null
194
+ if (descriptor?.get) {
195
+ c = renderEffect(() => applyStaticProp(el, key, (props as Record<string, unknown>)[key]))
196
+ } else {
197
+ c = applyProp(el, key, props[key])
198
+ }
180
199
  if (c) {
181
200
  if (!first) {
182
201
  first = c
@@ -205,6 +224,7 @@ export function applyProps(el: Element, props: Props): Cleanup | null {
205
224
  * Bind an event handler (onClick → "click") with batching + delegation support.
206
225
  */
207
226
  function applyEventProp(el: Element, key: string, value: unknown): Cleanup | null {
227
+ if (__DEV__) _countSink.__pyreon_count__?.('runtime.applyEvent')
208
228
  if (typeof value !== 'function') {
209
229
  // `undefined` and `null` are legitimate — conditional handler pattern:
210
230
  // <button onClick={condition ? handler : undefined}>
@@ -295,7 +315,13 @@ function applyStaticProp(el: Element, key: string, value: unknown): void {
295
315
  setStaticProp(el, key, value)
296
316
  }
297
317
 
318
+ // `runtime.applyProp` fires for EVERY prop key, including events. `runtime.applyEvent`
319
+ // fires only for `on*` props — strict subset. Useful diagnostic ratios:
320
+ // applyEvent / applyProp = event-handler density per element
321
+ // applyProp - applyEvent = static / reactive attr density
322
+ // Don't subtract them and treat as disjoint.
298
323
  export function applyProp(el: Element, key: string, value: unknown): Cleanup | null {
324
+ if (__DEV__) _countSink.__pyreon_count__?.('runtime.applyProp')
299
325
  // Event listener: onClick → "click"
300
326
  if (EVENT_RE.test(key)) return applyEventProp(el, key, value)
301
327
 
package/src/template.ts CHANGED
@@ -71,6 +71,7 @@ export function _bindText(
71
71
  source: { _v?: unknown; direct?: (fn: () => void) => () => void },
72
72
  node: Text,
73
73
  ): () => void {
74
+ if (__DEV__) _countSink.__pyreon_count__?.('runtime.bindText')
74
75
  // Fast path: source has .direct() (signal or computed)
75
76
  if (source.direct) {
76
77
  const textUpdate = () => {
@@ -112,6 +113,7 @@ export function _bindDirect(
112
113
  source: { _v?: unknown; direct?: (fn: () => void) => () => void },
113
114
  updater: (value: unknown) => void,
114
115
  ): () => void {
116
+ if (__DEV__) _countSink.__pyreon_count__?.('runtime.bindDirect')
115
117
  // Fast path: source has .direct() (signal or computed)
116
118
  if (source.direct) {
117
119
  updater(source._v)
@@ -0,0 +1,219 @@
1
+ /** @jsxImportSource @pyreon/core */
2
+ /**
3
+ * Reproduction of the deferred bug from PR #490 (queryReactiveKey-1000 journey).
4
+ *
5
+ * The pure-reactivity unit test (packages/core/reactivity/src/tests/fanout-repro.test.ts)
6
+ * passes — many effects subscribing to one signal all fire on every external
7
+ * .set. So the bug must involve actual Pyreon mount frames (provide/onMount/etc),
8
+ * not just the reactivity primitives.
9
+ *
10
+ * This test mounts a real Pyreon component with N effects subscribing to a
11
+ * shared signal, then writes to that signal in a tight external loop —
12
+ * mirroring the real shape from the queryReactiveKey-1000 journey.
13
+ */
14
+ import { For, h } from '@pyreon/core'
15
+ import { effect, signal } from '@pyreon/reactivity'
16
+ import { describe, expect, it } from 'vitest'
17
+ import { mount } from '../index'
18
+
19
+ describe('signal fan-out under tight external write loop — INSIDE mount frame', () => {
20
+ it('100 effects in a Pyreon component — each fires on every external .set', () => {
21
+ const sig = signal(0)
22
+ const counts = new Array(100).fill(0)
23
+
24
+ const root = document.createElement('div')
25
+ document.body.appendChild(root)
26
+
27
+ const Component = () => {
28
+ for (let i = 0; i < 100; i++) {
29
+ const idx = i
30
+ effect(() => {
31
+ sig()
32
+ counts[idx]++
33
+ })
34
+ }
35
+ return h('div', null, 'mounted')
36
+ }
37
+
38
+ const dispose = mount(h(Component, null), root)
39
+
40
+ // All 100 effects ran their initial setup.
41
+ for (const c of counts) expect(c).toBe(1)
42
+
43
+ // 10 external writes outside any batch.
44
+ for (let i = 1; i <= 10; i++) sig.set(i)
45
+
46
+ // Each effect should have re-fired 10 more times → total = 11.
47
+ let failed = 0
48
+ for (let i = 0; i < counts.length; i++) {
49
+ if (counts[i] !== 11) failed++
50
+ }
51
+ expect(failed, `effects with wrong count (out of 100)`).toBe(0)
52
+
53
+ dispose()
54
+ root.remove()
55
+ })
56
+
57
+ it('100 effects + an extra effect AFTER the loop — all fire on each .set', () => {
58
+ // Real-bug shape: a diagnostic effect placed AFTER the useQuery loop
59
+ // saw 0 re-runs across 10 flips, while a BEFORE-loop one saw 1 of 10.
60
+ const sig = signal(0)
61
+ const counts = new Array(100).fill(0)
62
+ let beforeRuns = 0
63
+ let afterRuns = 0
64
+
65
+ const root = document.createElement('div')
66
+ document.body.appendChild(root)
67
+
68
+ const Component = () => {
69
+ effect(() => {
70
+ sig()
71
+ beforeRuns++
72
+ })
73
+ for (let i = 0; i < 100; i++) {
74
+ const idx = i
75
+ effect(() => {
76
+ sig()
77
+ counts[idx]++
78
+ })
79
+ }
80
+ effect(() => {
81
+ sig()
82
+ afterRuns++
83
+ })
84
+ return h('div', null, 'mounted')
85
+ }
86
+
87
+ const dispose = mount(h(Component, null), root)
88
+
89
+ expect(beforeRuns).toBe(1)
90
+ expect(afterRuns).toBe(1)
91
+ for (const c of counts) expect(c).toBe(1)
92
+
93
+ for (let i = 1; i <= 10; i++) sig.set(i)
94
+
95
+ expect(beforeRuns, 'before-loop effect runs').toBe(11)
96
+ expect(afterRuns, 'after-loop effect runs').toBe(11)
97
+ let failed = 0
98
+ for (let i = 0; i < counts.length; i++) {
99
+ if (counts[i] !== 11) failed++
100
+ }
101
+ expect(failed, `effects with wrong count`).toBe(0)
102
+
103
+ dispose()
104
+ root.remove()
105
+ })
106
+
107
+ it('the body of each effect ALSO writes to a per-effect local signal (mimics useQuery slot writes)', () => {
108
+ // useQuery's effect body calls observer.setOptions which triggers the
109
+ // observer's subscribe callback which does batch(() => 9 signal.sets).
110
+ // Approximate that with: each outer effect's body creates its own local
111
+ // signal and writes to it in a batch.
112
+ const sig = signal(0)
113
+ const counts = new Array(100).fill(0)
114
+
115
+ const root = document.createElement('div')
116
+ document.body.appendChild(root)
117
+
118
+ const Component = () => {
119
+ for (let i = 0; i < 100; i++) {
120
+ const idx = i
121
+ const slot = signal('')
122
+ effect(() => {
123
+ sig() // subscribe
124
+ // Mimic batched writes inside the effect body
125
+ slot.set(`run-${counts[idx]}`)
126
+ counts[idx]++
127
+ })
128
+ }
129
+ return h('div', null, 'mounted')
130
+ }
131
+
132
+ const dispose = mount(h(Component, null), root)
133
+
134
+ for (const c of counts) expect(c).toBe(1)
135
+
136
+ for (let i = 1; i <= 10; i++) sig.set(i)
137
+
138
+ let failed = 0
139
+ for (let i = 0; i < counts.length; i++) {
140
+ if (counts[i] !== 11) failed++
141
+ }
142
+ expect(failed, `effects with wrong count`).toBe(0)
143
+
144
+ dispose()
145
+ root.remove()
146
+ })
147
+
148
+ // ─── The real bug shape: <For> wrapping the queries ───────────────────────
149
+ //
150
+ // mountFor (`packages/core/runtime-dom/src/nodes.ts`) wraps its body in
151
+ // effect() but does NOT untrack the child mountChild calls (mountReactive
152
+ // does — line 92 of nodes.ts). As a result, any signal read during a
153
+ // child component's setup tracks against the For effect's run. When the
154
+ // tracked signal flips, For's effect re-runs → runCleanup() disposes ALL
155
+ // inner effects (the per-item setOptions effects) → handleIncrementalUpdate
156
+ // sees keys unchanged → does not re-mount → setOptions effects gone +
157
+ // never recreated. signalWrite fires N times but effectRun stays at the
158
+ // initial-mount count — the exact PR #490 observation.
159
+
160
+ it('REGRESSION: 100 effects mounted under <For> re-fire when shared signal flips', () => {
161
+ // Mirrors the queryReactiveKey-1000 shape: a mode/count tuple keys the
162
+ // For so its body mounts QueryAtScale-equivalent ONCE. Inside, N effects
163
+ // subscribe to a separate `reactKey` signal. External flips of reactKey
164
+ // must propagate to all N inner effects.
165
+ const reactKey = signal(0)
166
+ const counts = new Array(100).fill(0)
167
+ const root = document.createElement('div')
168
+ document.body.appendChild(root)
169
+
170
+ // Stable single-item array — For mounts the inner component exactly once.
171
+ const items = [{ id: 1 }] as const
172
+ type Item = (typeof items)[number]
173
+
174
+ const Inner = (props: { item: Item }) => {
175
+ // Read props.item to keep the component honest — same shape as
176
+ // QueryAtScale (which reads props.mode + props.count). That read
177
+ // tracks against the OUTER For effect via makeReactiveProps' getter.
178
+ void props.item.id
179
+
180
+ // Mimic useQuery's "read signal at construction time, OUTSIDE the
181
+ // inner effect" pattern. This is what `new QueryObserver(client,
182
+ // options())` does — options() reads reactKey while activeEffect is
183
+ // the outer effect (For's run), leaking the subscription up.
184
+ const _seed = reactKey() // ← this is the leak
185
+
186
+ // Plus: 100 effects each subscribing to reactKey via their own bodies.
187
+ for (let i = 0; i < 100; i++) {
188
+ const idx = i
189
+ effect(() => {
190
+ reactKey()
191
+ counts[idx]++
192
+ })
193
+ }
194
+ return h('div', { 'data-testid': 'inner' }, `mounted ${_seed}`)
195
+ }
196
+
197
+ const dispose = mount(
198
+ h(For, {
199
+ each: items,
200
+ by: (it: Item) => it.id,
201
+ children: (it: Item) => h(Inner, { item: it }),
202
+ }),
203
+ root,
204
+ )
205
+
206
+ for (const c of counts) expect(c).toBe(1)
207
+
208
+ for (let i = 1; i <= 10; i++) reactKey.set(i)
209
+
210
+ let failed = 0
211
+ for (let i = 0; i < counts.length; i++) {
212
+ if (counts[i] !== 11) failed++
213
+ }
214
+ expect(failed, `effects with wrong count after 10 flips`).toBe(0)
215
+
216
+ dispose()
217
+ root.remove()
218
+ })
219
+ })
@@ -1611,7 +1611,7 @@ describe('mount — edge cases', () => {
1611
1611
 
1612
1612
  test('mounting array of children', () => {
1613
1613
  const el = container()
1614
- mount(h('div', null, ...[h('span', null, 'a'), h('span', null, 'b'), h('span', null, 'c')]), el)
1614
+ mount(h('div', null, h('span', null, 'a'), h('span', null, 'b'), h('span', null, 'c')), el)
1615
1615
  expect(el.querySelectorAll('span').length).toBe(3)
1616
1616
  })
1617
1617
 
@@ -118,8 +118,16 @@ describe('Transition', () => {
118
118
  const target = el.querySelector('.lifecycle') as HTMLElement
119
119
  if (target) {
120
120
  target.dispatchEvent(new Event('transitionend'))
121
- await new Promise<void>((r) => setTimeout(r, 10))
122
- expect(onAfterEnter).toHaveBeenCalled()
121
+ // Poll for the assertion instead of fixed sleep. Fixed setTimeout
122
+ // is structurally flaky on shared CI runners: scheduling latency
123
+ // between dispatchEvent's callback queue and the next tick can
124
+ // exceed any reasonable fixed wait (we tried 10ms then 50ms, both
125
+ // flaked). `vi.waitFor` polls every 10ms up to the timeout, so it
126
+ // settles as soon as the assertion holds while still bounding the
127
+ // worst case.
128
+ await vi.waitFor(() => expect(onAfterEnter).toHaveBeenCalled(), {
129
+ timeout: 2000,
130
+ })
123
131
  }
124
132
  })
125
133
 
@@ -199,12 +199,22 @@ export function TransitionGroup<T = unknown>(props: TransitionGroupProps<T>): VN
199
199
  const key = props.keyFn(item, i)
200
200
  if (entries.has(key)) continue
201
201
  const itemRef = createRef<HTMLElement>()
202
- const rawVNode = runUntracked(() => props.render(item, i))
203
- const vnode: VNode =
204
- typeof rawVNode.type === 'string'
205
- ? { ...rawVNode, props: { ...rawVNode.props, ref: itemRef } as Props }
206
- : rawVNode
207
- const cleanup = mountChild(vnode, container, null)
202
+ // Both render AND mountChild must run untracked — child component
203
+ // setup (signal reads inside the render callback's resulting tree,
204
+ // useTheme / useQuery's options() construction etc.) must NOT
205
+ // subscribe this effect. Otherwise an unrelated signal flip re-runs
206
+ // the TransitionGroup effect, runCleanup() disposes the children's
207
+ // inner effects, and the next mount path skips re-rendering kept
208
+ // entries → the inner reactivity is lost. Same shape as the
209
+ // mountFor / mountKeyedList fix in nodes.ts.
210
+ const cleanup = runUntracked(() => {
211
+ const rawVNode = props.render(item, i)
212
+ const vnode: VNode =
213
+ typeof rawVNode.type === 'string'
214
+ ? { ...rawVNode, props: { ...rawVNode.props, ref: itemRef } as Props }
215
+ : rawVNode
216
+ return mountChild(vnode, container, null)
217
+ })
208
218
  const entry: ItemEntry = { key, ref: itemRef, cleanup, leaving: false, cancelTransition: null }
209
219
  entries.set(key, entry)
210
220
  newEntries.push(entry)