ripple 0.3.84 → 0.3.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/package.json +3 -3
  3. package/src/constants.js +4 -0
  4. package/src/runtime/internal/client/blocks.js +53 -0
  5. package/src/runtime/internal/client/component.js +3 -3
  6. package/src/runtime/internal/client/constants.js +4 -0
  7. package/src/runtime/internal/client/for.js +87 -14
  8. package/src/runtime/internal/client/if.js +18 -2
  9. package/src/runtime/internal/client/index.js +1 -0
  10. package/src/runtime/internal/client/operations.js +25 -0
  11. package/src/runtime/internal/client/runtime.js +62 -10
  12. package/src/runtime/internal/client/switch.js +33 -5
  13. package/src/runtime/internal/client/template.js +10 -2
  14. package/src/runtime/internal/client/try.js +12 -2
  15. package/src/runtime/internal/client/types.d.ts +4 -0
  16. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -2
  17. package/tests/client/compiler/compiler.basic.test.tsrx +5 -2
  18. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -4
  19. package/tests/client/for-single-root-items.test.tsrx +130 -0
  20. package/tests/client/scoped-flush.test.tsrx +105 -0
  21. package/tests/hydration/compiled/client/basic.js +198 -214
  22. package/tests/hydration/compiled/client/composite.js +23 -72
  23. package/tests/hydration/compiled/client/for.js +66 -72
  24. package/tests/hydration/compiled/client/hmr.js +2 -13
  25. package/tests/hydration/compiled/client/html.js +619 -479
  26. package/tests/hydration/compiled/client/if-children.js +72 -84
  27. package/tests/hydration/compiled/client/if-fragment-controlflow.js +463 -0
  28. package/tests/hydration/compiled/client/if.js +87 -92
  29. package/tests/hydration/compiled/client/mixed-control-flow.js +120 -160
  30. package/tests/hydration/compiled/client/nested-control-flow.js +288 -360
  31. package/tests/hydration/compiled/client/portal.js +15 -21
  32. package/tests/hydration/compiled/client/reactivity.js +7 -12
  33. package/tests/hydration/compiled/client/switch.js +113 -115
  34. package/tests/hydration/compiled/client/track-async-serialization.js +71 -122
  35. package/tests/hydration/compiled/client/try.js +26 -41
  36. package/tests/hydration/compiled/server/basic.js +295 -556
  37. package/tests/hydration/compiled/server/composite.js +31 -50
  38. package/tests/hydration/compiled/server/events.js +37 -173
  39. package/tests/hydration/compiled/server/for.js +236 -847
  40. package/tests/hydration/compiled/server/head.js +110 -241
  41. package/tests/hydration/compiled/server/hmr.js +14 -41
  42. package/tests/hydration/compiled/server/html-in-template.js +14 -38
  43. package/tests/hydration/compiled/server/html.js +951 -1176
  44. package/tests/hydration/compiled/server/if-children.js +59 -493
  45. package/tests/hydration/compiled/server/if-fragment-controlflow.js +227 -0
  46. package/tests/hydration/compiled/server/if.js +57 -209
  47. package/tests/hydration/compiled/server/mixed-control-flow.js +144 -250
  48. package/tests/hydration/compiled/server/nested-control-flow.js +309 -559
  49. package/tests/hydration/compiled/server/portal.js +94 -156
  50. package/tests/hydration/compiled/server/reactivity.js +23 -65
  51. package/tests/hydration/compiled/server/return.js +6 -16
  52. package/tests/hydration/compiled/server/switch.js +91 -219
  53. package/tests/hydration/compiled/server/track-async-serialization.js +211 -256
  54. package/tests/hydration/compiled/server/try.js +67 -126
  55. package/tests/hydration/components/html.tsrx +75 -0
  56. package/tests/hydration/components/if-fragment-controlflow.tsrx +145 -0
  57. package/tests/hydration/html.test.js +50 -0
  58. package/tests/hydration/if-fragment-controlflow.test.js +95 -0
  59. package/tests/server/compiler.test.tsrx +6 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.86
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1310](https://github.com/Ripple-TS/ripple/pull/1310)
8
+ [`4ebd58d`](https://github.com/Ripple-TS/ripple/commit/4ebd58dfe853c1ed945072822eaba8a7a9e19a6c)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Fix
10
+ `Cannot use 'in' operator to search for 'parent' in null` thrown from `append()`
11
+ when reordering or inserting into a keyed `@for` whose item body is a single
12
+ control-flow / component root (e.g. `@for (...; key ...) { <Card {item} /> }`
13
+ where `Card`'s own body is a single `@if`/`@for`/component). The 0.3.85
14
+ wrapper-anchor optimization leaves such an item block's `s.start` null because
15
+ its DOM is rendered through a descendant block, and keyed reconciliation read
16
+ `s.start` directly as the insertion anchor. Reconciliation now resolves the real
17
+ first/last DOM node by descending child blocks, so no `<!>` comment marker is
18
+ reintroduced and the optimization's reduced DOM-mutation cost is preserved. The
19
+ same resolution is applied to `@switch` case reordering.
20
+ - Updated dependencies
21
+ [[`e4e6d7b`](https://github.com/Ripple-TS/ripple/commit/e4e6d7b854786ad19a2c86276ea7e0ffb062e61a)]:
22
+ - @tsrx/ripple@0.1.34
23
+
24
+ ## 0.3.85
25
+
26
+ ### Patch Changes
27
+
28
+ - [#1307](https://github.com/Ripple-TS/ripple/pull/1307)
29
+ [`f55466b`](https://github.com/Ripple-TS/ripple/commit/f55466bde65d0cff00c0c4525af9d68ae794ffd2)
30
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Skip the wrapper anchor for
31
+ single control-flow / code-block / component root scopes. When a scope's entire
32
+ renderable output is a single `@if`, `@switch`, `@for`, `@try`, or static child
33
+ component — i.e. a component body, a control-flow branch, or a `@{}` body whose
34
+ only output after setup is one of these — the compiler now renders it directly
35
+ before the parent-provided `__anchor` instead of synthesizing a `<!>` fragment
36
+ wrapper and an extra append + clone. For deep recursive trees this measurably
37
+ cuts mount time and shrinks generated output; in the recursive-context benchmark
38
+ it brought mount DOM operations to one clone + one append per element (from
39
+ ~1.5×) and halved the comment-anchor nodes.
40
+
41
+ Hydration is preserved. The control-flow runtimes
42
+ (`if_block`/`switch_block`/`for_block`/`for_block_keyed`/`try_block`) capture
43
+ the SSR boundary marker and hand it to `append()` afterward, so the existing
44
+ context-aware cursor advance still runs — including for a root scope used as a
45
+ child of a composite/slot with following siblings. Single-component roots need
46
+ no runtime change at all, since a component's own content advances the hydration
47
+ cursor.
48
+
49
+ Also relaxes the compiler's text-expression detection: `string + anything` (e.g.
50
+ `{a + '|' + b}`) is now recognized as text and lowered to the fast `set_text`
51
+ path without requiring an explicit `as string`, since such an expression always
52
+ evaluates to a string in JS.
53
+
54
+ - [#1307](https://github.com/Ripple-TS/ripple/pull/1307)
55
+ [`f55466b`](https://github.com/Ripple-TS/ripple/commit/f55466bde65d0cff00c0c4525af9d68ae794ffd2)
56
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Scope the client flush
57
+ traversal to the updated subtree. Previously every flush walked the whole root
58
+ block tree to find dirty subscribers, so a deeply-scoped update (e.g. mutating
59
+ state read by only a small subtree) paid a cost proportional to the entire tree
60
+ rather than the affected branch. `flush_updates` now descends only along the
61
+ routing path to each directly-scheduled block and fully scans just that block's
62
+ subtree, where its subscribers live. A tracked read from outside its owner's
63
+ subtree (e.g. smuggled across sibling subtrees via a module-level variable) is
64
+ detected in `register_dependency` and transparently falls back to the original
65
+ full-tree scan, so behavior is unchanged.
66
+
67
+ - Updated dependencies
68
+ [[`ba498cd`](https://github.com/Ripple-TS/ripple/commit/ba498cde76e9f83235ce91da825f403a28441bff),
69
+ [`313b351`](https://github.com/Ripple-TS/ripple/commit/313b3513e4a959dd80b546da41c798066c5ccb0f),
70
+ [`35ac700`](https://github.com/Ripple-TS/ripple/commit/35ac70052d79efae41bb1df2440fee3f052ca115),
71
+ [`bbe6e74`](https://github.com/Ripple-TS/ripple/commit/bbe6e7422c690558f0dfcb3abe5452d4f4cdde91),
72
+ [`0e9f523`](https://github.com/Ripple-TS/ripple/commit/0e9f52358a615c2fc7759544e96c43dccb533c86),
73
+ [`35ac700`](https://github.com/Ripple-TS/ripple/commit/35ac70052d79efae41bb1df2440fee3f052ca115),
74
+ [`35ac700`](https://github.com/Ripple-TS/ripple/commit/35ac70052d79efae41bb1df2440fee3f052ca115),
75
+ [`2b65285`](https://github.com/Ripple-TS/ripple/commit/2b65285bfcd4c6a0aa93d7fa0b25082e6ec74e1f),
76
+ [`35ac700`](https://github.com/Ripple-TS/ripple/commit/35ac70052d79efae41bb1df2440fee3f052ca115),
77
+ [`f55466b`](https://github.com/Ripple-TS/ripple/commit/f55466bde65d0cff00c0c4525af9d68ae794ffd2),
78
+ [`b887deb`](https://github.com/Ripple-TS/ripple/commit/b887debf5f47e63d73184ac218ec8b3542a5e21c),
79
+ [`3668c5f`](https://github.com/Ripple-TS/ripple/commit/3668c5fe9cdaca4862707d653d23af94780f42af),
80
+ [`bbc3843`](https://github.com/Ripple-TS/ripple/commit/bbc384387e33c538234be36c07cc4b30ef6ce136)]:
81
+ - @tsrx/ripple@0.1.33
82
+ - @tsrx/core@0.1.33
83
+
3
84
  ## 0.3.84
4
85
 
5
86
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.84",
6
+ "version": "0.3.86",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -73,8 +73,8 @@
73
73
  "clsx": "^2.1.1",
74
74
  "devalue": "^5.8.1",
75
75
  "esm-env": "^1.2.2",
76
- "@tsrx/core": "0.1.32",
77
- "@tsrx/ripple": "0.1.32"
76
+ "@tsrx/core": "0.1.33",
77
+ "@tsrx/ripple": "0.1.34"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@types/estree": "^1.0.8",
package/src/constants.js CHANGED
@@ -2,6 +2,10 @@ export const TEMPLATE_FRAGMENT = 1;
2
2
  export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
3
3
  export const IS_CONTROLLED = 1 << 2;
4
4
  export const IS_INDEXED = 1 << 3;
5
+ // A @for whose component body is its sole root: renders before the parent
6
+ // `__anchor` (sibling semantics, no `<!>` wrapper). Distinct from IS_CONTROLLED,
7
+ // which renders inside a parent element.
8
+ export const ROOT_CONTROLLED = 1 << 4;
5
9
  export const TEMPLATE_SVG_NAMESPACE = 1 << 5;
6
10
  export const TEMPLATE_MATHML_NAMESPACE = 1 << 6;
7
11
 
@@ -429,6 +429,59 @@ export function move_block(block, target) {
429
429
  return moved;
430
430
  }
431
431
 
432
+ /**
433
+ * Resolve the first DOM node owned by a block. A block normally records its
434
+ * range in `s.start`/`s.end`, but an optimized single control-flow / component
435
+ * root scope renders its content through a descendant block instead of a
436
+ * synthesized `<!>` wrapper, so its own `s.start` is null. In that case we
437
+ * descend into child blocks to find the real first node. Returns null when the
438
+ * block currently renders no DOM.
439
+ * @param {Block} block
440
+ * @returns {Node | null}
441
+ */
442
+ export function get_first_node(block) {
443
+ var f = block.f;
444
+ if ((f & BRANCH_BLOCK) !== 0 && (f & TRY_BLOCK) === 0) {
445
+ var s = block.s;
446
+ if (s !== null && s.start !== null) {
447
+ return s.start;
448
+ }
449
+ }
450
+ var child = block.first;
451
+ while (child !== null) {
452
+ var node = get_first_node(child);
453
+ if (node !== null) {
454
+ return node;
455
+ }
456
+ child = child.next;
457
+ }
458
+ return null;
459
+ }
460
+
461
+ /**
462
+ * Mirror of {@link get_first_node} for the last DOM node owned by a block.
463
+ * @param {Block} block
464
+ * @returns {Node | null}
465
+ */
466
+ export function get_last_node(block) {
467
+ var f = block.f;
468
+ if ((f & BRANCH_BLOCK) !== 0 && (f & TRY_BLOCK) === 0) {
469
+ var s = block.s;
470
+ if (s !== null && s.start !== null) {
471
+ return s.end;
472
+ }
473
+ }
474
+ var child = block.last;
475
+ while (child !== null) {
476
+ var node = get_last_node(child);
477
+ if (node !== null) {
478
+ return node;
479
+ }
480
+ child = child.prev;
481
+ }
482
+ return null;
483
+ }
484
+
432
485
  /**
433
486
  * @param {Block} block
434
487
  * @param {boolean} [remove_dom]
@@ -1,4 +1,4 @@
1
- /** @import { Block } from '#client' */
1
+ /** @import { AppendIntoAnchor, Block } from '#client' */
2
2
 
3
3
  import { is_tsrx_element } from '../../element.js';
4
4
  import { render_value } from './expression.js';
@@ -6,7 +6,7 @@ import { active_block, pop_component, push_component } from './runtime.js';
6
6
 
7
7
  /**
8
8
  * @param {Function} fn
9
- * @param {Node} anchor
9
+ * @param {Node | AppendIntoAnchor} anchor
10
10
  * @param {Record<string, any>} props
11
11
  * @param {Block | null} [block=active_block]
12
12
  * @returns {void}
@@ -21,7 +21,7 @@ export function render_component(fn, anchor, props, block = active_block) {
21
21
 
22
22
  /**
23
23
  * @param {Function} fn
24
- * @param {Node} anchor
24
+ * @param {Node | AppendIntoAnchor} anchor
25
25
  * @param {Record<string, any>} props
26
26
  * @param {Block | null} [block=active_block]
27
27
  * @returns {void}
@@ -19,6 +19,10 @@ export var DERIVED = 1 << 18;
19
19
  export var DEFERRED = 1 << 19;
20
20
  export var PAUSED = 1 << 20;
21
21
  export var DESTROYED = 1 << 21;
22
+ // Marks a block that was directly scheduled (the owner of a mutated tracked).
23
+ // Its whole subtree is scanned for dirty subscribers during flush; blocks above
24
+ // it are only traversed along the CONTAINS_UPDATE routing path.
25
+ export var UPDATE_SOURCE = 1 << 22;
22
26
 
23
27
  export var CONTROL_FLOW_BLOCK = FOR_BLOCK | IF_BLOCK | SWITCH_BLOCK | TRY_BLOCK | COMPOSITE_BLOCK;
24
28
 
@@ -1,10 +1,18 @@
1
1
  /** @import { Block, Tracked } from '#client' */
2
2
 
3
- import { IS_CONTROLLED, IS_INDEXED } from '../../../constants.js';
4
- import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
3
+ import { IS_CONTROLLED, IS_INDEXED, ROOT_CONTROLLED } from '../../../constants.js';
4
+ import {
5
+ branch,
6
+ destroy_block,
7
+ destroy_block_children,
8
+ get_first_node,
9
+ get_last_node,
10
+ render,
11
+ } from './blocks.js';
5
12
  import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
6
13
  import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
7
14
  import { create_text, get_first_child, get_last_child, next_sibling } from './operations.js';
15
+ import { append } from './template.js';
8
16
  import { active_block, set, tracked, untrack } from './runtime.js';
9
17
  import { array_from, is_array } from '@tsrx/core/runtime/language-helpers';
10
18
 
@@ -70,12 +78,26 @@ function create_empty(anchor, render_empty) {
70
78
 
71
79
  /**
72
80
  * @param {Block} block
73
- * @param {Element} anchor
81
+ * @param {ChildNode} anchor
74
82
  * @returns {void}
75
83
  */
76
84
  function move(block, anchor) {
77
- var node = block.s.start;
78
- var end = block.s.end;
85
+ // Fast path: a normal item records its own range. Only an optimized single
86
+ // control-flow / component root item (DOM rendered through a descendant
87
+ // block, `s.start` null) needs the descent via `get_first_node`/`get_last_node`.
88
+ var s = block.s;
89
+ var node = s.start;
90
+ var end;
91
+
92
+ if (node === null) {
93
+ node = get_first_node(block);
94
+ if (node === null) {
95
+ return;
96
+ }
97
+ end = get_last_node(block);
98
+ } else {
99
+ end = s.end;
100
+ }
79
101
 
80
102
  if (node === end) {
81
103
  anchor.before(node);
@@ -86,12 +108,43 @@ function move(block, anchor) {
86
108
  anchor.before(node);
87
109
  node = next_node;
88
110
  if (node === end) {
89
- anchor.before(end);
111
+ anchor.before(/** @type {Node} */ (end));
90
112
  break;
91
113
  }
92
114
  }
93
115
  }
94
116
 
117
+ /**
118
+ * Resolve the insertion anchor for a block at `index`: the first real DOM node
119
+ * at or after `index`, or `fallback` when every remaining block renders nothing.
120
+ * Scanning forward keeps insertions correct even when an optimized item renders
121
+ * no DOM (e.g. a single `@if` that is currently false), which previously relied
122
+ * on a synthesized `<!>` wrapper as a stable position marker.
123
+ * @param {Block[]} blocks
124
+ * @param {number} index
125
+ * @param {number} length
126
+ * @param {Element | Text} fallback
127
+ * @returns {ChildNode}
128
+ */
129
+ function block_start(blocks, index, length, fallback) {
130
+ if (index >= length) {
131
+ return fallback;
132
+ }
133
+ // Fast path: a normal item records its own boundary, so this is the same
134
+ // single property read the pre-#1307 code did — no descent, no scan.
135
+ var first = blocks[index].s.start;
136
+ if (first !== null) {
137
+ return first;
138
+ }
139
+ for (var k = index; k < length; k++) {
140
+ var node = get_first_node(blocks[k]);
141
+ if (node !== null) {
142
+ return /** @type {ChildNode} */ (node);
143
+ }
144
+ }
145
+ return fallback;
146
+ }
147
+
95
148
  /**
96
149
  * @template V
97
150
  * @param {V[] | Iterable<V>} collection
@@ -121,7 +174,10 @@ function collection_to_array(collection) {
121
174
  export function for_block(node, get_collection, render_fn, flags, render_empty) {
122
175
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
123
176
  var is_indexed = (flags & IS_INDEXED) !== 0;
177
+ var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
124
178
  var anchor = /** @type {Element | Text} */ (node);
179
+ /** @type {Node | undefined} */
180
+ var boundary;
125
181
 
126
182
  if (is_controlled) {
127
183
  if (hydrating) {
@@ -133,6 +189,9 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
133
189
  }
134
190
 
135
191
  if (hydrating) {
192
+ if (root_controlled) {
193
+ boundary = /** @type {Node} */ (hydrate_node);
194
+ }
136
195
  hydrate_next();
137
196
  }
138
197
 
@@ -153,6 +212,10 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
153
212
  null,
154
213
  FOR_BLOCK,
155
214
  );
215
+
216
+ if (hydrating && root_controlled) {
217
+ append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
218
+ }
156
219
  }
157
220
 
158
221
  /**
@@ -169,7 +232,10 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
169
232
  export function for_block_keyed(node, get_collection, render_fn, flags, get_key, render_empty) {
170
233
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
171
234
  var is_indexed = (flags & IS_INDEXED) !== 0;
235
+ var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
172
236
  var anchor = /** @type {Element | Text} */ (node);
237
+ /** @type {Node | undefined} */
238
+ var boundary;
173
239
 
174
240
  if (is_controlled) {
175
241
  var parent_node = /** @type {Element} */ (node);
@@ -183,6 +249,9 @@ export function for_block_keyed(node, get_collection, render_fn, flags, get_key,
183
249
  }
184
250
 
185
251
  if (hydrating) {
252
+ if (root_controlled) {
253
+ boundary = /** @type {Node} */ (hydrate_node);
254
+ }
186
255
  hydrate_next();
187
256
  }
188
257
 
@@ -208,6 +277,10 @@ export function for_block_keyed(node, get_collection, render_fn, flags, get_key,
208
277
  null,
209
278
  FOR_BLOCK,
210
279
  );
280
+
281
+ if (hydrating && root_controlled) {
282
+ append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
283
+ }
211
284
  }
212
285
 
213
286
  /**
@@ -408,7 +481,7 @@ function reconcile_by_key(
408
481
  if (j <= b_end) {
409
482
  while (j <= b_end) {
410
483
  b_val = b[j];
411
- var target = j >= a_length ? anchor : a_blocks[j].s.start;
484
+ var target = block_start(a_blocks, j, a_length, anchor);
412
485
  b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, true);
413
486
  j++;
414
487
  }
@@ -527,14 +600,14 @@ function reconcile_by_key(
527
600
  b_val = b[pos];
528
601
  next_pos = pos + 1;
529
602
 
530
- var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
603
+ var target = block_start(b_blocks, next_pos, b_length, anchor);
531
604
  b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
532
605
  } else if (j < 0 || i !== seq[j]) {
533
606
  pos = i + b_start;
534
607
  b_val = b[pos];
535
608
  next_pos = pos + 1;
536
609
 
537
- var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
610
+ var target = block_start(b_blocks, next_pos, b_length, anchor);
538
611
  move(b_blocks[pos], target);
539
612
  } else {
540
613
  j--;
@@ -547,7 +620,7 @@ function reconcile_by_key(
547
620
  b_val = b[pos];
548
621
  next_pos = pos + 1;
549
622
 
550
- var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
623
+ var target = block_start(b_blocks, next_pos, b_length, anchor);
551
624
  b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
552
625
  }
553
626
  }
@@ -695,7 +768,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
695
768
  if (j <= b_end) {
696
769
  while (j <= b_end) {
697
770
  b_val = b[j];
698
- var target = j >= a_length ? anchor : a_blocks[j].s.start;
771
+ var target = block_start(a_blocks, j, a_length, anchor);
699
772
  b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, false);
700
773
  j++;
701
774
  }
@@ -808,14 +881,14 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
808
881
  b_val = b[pos];
809
882
  next_pos = pos + 1;
810
883
 
811
- var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
884
+ var target = block_start(b_blocks, next_pos, b_length, anchor);
812
885
  b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
813
886
  } else if (j < 0 || i !== seq[j]) {
814
887
  pos = i + b_start;
815
888
  b_val = b[pos];
816
889
  next_pos = pos + 1;
817
890
 
818
- var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
891
+ var target = block_start(b_blocks, next_pos, b_length, anchor);
819
892
  move(b_blocks[pos], target);
820
893
  } else {
821
894
  j--;
@@ -828,7 +901,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
828
901
  b_val = b[pos];
829
902
  next_pos = pos + 1;
830
903
 
831
- var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
904
+ var target = block_start(b_blocks, next_pos, b_length, anchor);
832
905
  b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
833
906
  }
834
907
  }
@@ -2,15 +2,27 @@
2
2
 
3
3
  import { branch, destroy_block, render } from './blocks.js';
4
4
  import { IF_BLOCK, UNINITIALIZED } from './constants.js';
5
- import { hydrate_next, hydrating } from './hydration.js';
5
+ import { hydrate_next, hydrate_node, hydrating } from './hydration.js';
6
+ import { append } from './template.js';
6
7
 
7
8
  /**
8
9
  * @param {Node} node
9
10
  * @param {(set_branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
11
+ * @param {boolean} [root_controlled] When true the block renders directly before
12
+ * the component's `__anchor` (no synthesized `<!>` wrapper). During hydration
13
+ * the SSR boundary start marker sits at the cursor; we hand it to `append()`
14
+ * afterwards so it performs the same context-aware boundary advance the
15
+ * eliminated wrapper's `append()` used to do.
10
16
  * @returns {void}
11
17
  */
12
- export function if_block(node, fn) {
18
+ export function if_block(node, fn, root_controlled) {
19
+ /** @type {Node | undefined} */
20
+ var boundary;
21
+
13
22
  if (hydrating) {
23
+ if (root_controlled) {
24
+ boundary = /** @type {Node} */ (hydrate_node);
25
+ }
14
26
  hydrate_next();
15
27
  }
16
28
 
@@ -52,4 +64,8 @@ export function if_block(node, fn) {
52
64
  null,
53
65
  IF_BLOCK,
54
66
  );
67
+
68
+ if (hydrating && root_controlled) {
69
+ append(/** @type {ChildNode} */ (anchor), /** @type {Node} */ (boundary));
70
+ }
55
71
  }
@@ -2,6 +2,7 @@ export {
2
2
  first_child as child,
3
3
  first_child_frag,
4
4
  next_sibling as sibling,
5
+ append_into,
5
6
  document,
6
7
  create_text,
7
8
  init_operations,
@@ -1,3 +1,5 @@
1
+ /** @import { AppendIntoAnchor } from '#client' */
2
+
1
3
  import { TEXT_NODE } from '../../../constants.js';
2
4
  import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
3
5
  import { get_descriptor } from '@tsrx/core/runtime/language-helpers';
@@ -83,6 +85,29 @@ export function first_child(node, is_text) {
83
85
  return child;
84
86
  }
85
87
 
88
+ /**
89
+ * Anchor sentinel for all-component children: components render directly into
90
+ * `parent` instead of each inserting before a placeholder comment. `append()`
91
+ * detects the sentinel (no `nodeType`) and appendChild()s into `parent`. During
92
+ * hydration we descend the cursor into `parent` (mirroring first_child) so the
93
+ * first appended component adopts the server's first child of `parent`.
94
+ * @param {Node} parent
95
+ * @returns {AppendIntoAnchor}
96
+ */
97
+ export function append_into(parent) {
98
+ if (hydrating) {
99
+ var child = get_first_child(/** @type {Node} */ (hydrate_node));
100
+
101
+ if (child === null) {
102
+ child = /** @type {Node} */ (hydrate_node).appendChild(create_text());
103
+ }
104
+
105
+ set_hydrate_node(child);
106
+ }
107
+
108
+ return { parent };
109
+ }
110
+
86
111
  /**
87
112
  * @template {Node} N
88
113
  * @param {N} node
@@ -34,6 +34,7 @@ import {
34
34
  SUSPENSE_REJECTED,
35
35
  TRY_BLOCK,
36
36
  DIRECT_CHILD_BLOCK,
37
+ UPDATE_SOURCE,
37
38
  } from './constants.js';
38
39
  import {
39
40
  begin_boundary_request,
@@ -93,6 +94,12 @@ let is_micro_task_queued = false;
93
94
  let clock = 0;
94
95
  /** @type {Block[]} */
95
96
  let queued_root_blocks = [];
97
+ // Flush normally only scans the subtree of each directly-scheduled block, since a
98
+ // tracked's subscribers live inside the subtree of the block that owns it. If a
99
+ // tracked is ever read from outside its owner's subtree (e.g. smuggled across
100
+ // sibling subtrees via a module-level variable), that invariant breaks, so we
101
+ // permanently fall back to scanning the whole root tree to stay correct.
102
+ let disable_scoped_flush = false;
96
103
  /** @type {(() => void)[]} */
97
104
  let queued_microtasks = [];
98
105
  /** @type {number} */
@@ -939,22 +946,33 @@ function trigger_track_get(fn, v) {
939
946
  function flush_updates(root_block) {
940
947
  /** @type {Block | null} */
941
948
  var current = root_block;
942
- var containing_update = null;
943
949
  var pre_effects = [];
944
950
  var other_blocks = [];
945
951
  var effects = [];
946
- var containing_update_head = null;
952
+ // The nearest enclosing directly-scheduled block ("update source"). While it is
953
+ // non-null we are inside a source's subtree and scan every descendant. Above
954
+ // sources we only follow the CONTAINS_UPDATE routing path, so sibling subtrees
955
+ // that contain no update are skipped. When scoping is disabled we treat the
956
+ // whole root tree as one source — the original full-tree scan.
957
+ /** @type {Block | null} */
958
+ var scope_root = disable_scoped_flush ? root_block : null;
947
959
 
948
960
  while (current !== null) {
949
961
  var flags = current.f;
962
+ var on_path = (flags & CONTAINS_UPDATE) !== 0;
950
963
 
951
- if ((flags & CONTAINS_UPDATE) !== 0) {
964
+ if (on_path) {
952
965
  current.f ^= CONTAINS_UPDATE;
953
- containing_update_head = { v: containing_update, n: containing_update_head };
954
- containing_update = current;
955
966
  }
956
967
 
957
- if ((flags & PAUSED) === 0 && containing_update !== null) {
968
+ if ((flags & UPDATE_SOURCE) !== 0) {
969
+ current.f ^= UPDATE_SOURCE;
970
+ if (scope_root === null) {
971
+ scope_root = current;
972
+ }
973
+ }
974
+
975
+ if ((flags & PAUSED) === 0 && (on_path || scope_root !== null)) {
958
976
  if ((flags & PRE_EFFECT_BLOCK) !== 0) {
959
977
  pre_effects.push(current);
960
978
  } else if ((flags & EFFECT_BLOCK) !== 0) {
@@ -976,10 +994,8 @@ function flush_updates(root_block) {
976
994
  current = current.next;
977
995
 
978
996
  while (current === null && parent !== null) {
979
- if (parent === containing_update) {
980
- var head = /** @type {{ v: Block | null, n: any }} */ (containing_update_head);
981
- containing_update = head.v;
982
- containing_update_head = head.n;
997
+ if (parent === scope_root) {
998
+ scope_root = null;
983
999
  }
984
1000
  current = parent.next;
985
1001
  parent = parent.p;
@@ -1115,6 +1131,10 @@ export function schedule_update(block) {
1115
1131
  if (scheduler_mode === FLUSH_MICROTASK) {
1116
1132
  queue_microtask();
1117
1133
  }
1134
+ // The scheduled block roots the subtree that flush_updates scans for dirty
1135
+ // subscribers. Mark it even if it (or an ancestor) is already on a routing
1136
+ // path, so the early return below still records it as a scan root.
1137
+ block.f |= UPDATE_SOURCE;
1118
1138
  let current = block;
1119
1139
 
1120
1140
  while (current !== null) {
@@ -1134,6 +1154,38 @@ export function schedule_update(block) {
1134
1154
  * @param {Tracked | Derived} tracked
1135
1155
  */
1136
1156
  function register_dependency(tracked) {
1157
+ if (!disable_scoped_flush && active_block !== null && active_block !== tracked.b) {
1158
+ // Scoped flush only scans the owner's subtree for dirty subscribers, valid
1159
+ // only while every subscriber lives inside it. The subscriber↔owner
1160
+ // ancestry is structural and stable for a block's lifetime (its `.p` chain
1161
+ // never changes — DOM moves don't reparent blocks), so we only need to
1162
+ // verify *new* dependency edges: if this reaction already depended on
1163
+ // `tracked` on its previous run (it's in the prior chain), the ancestry
1164
+ // walk was already done — skip it. This keeps the (possibly deep) walk off
1165
+ // the hot path for the common case of stable subscriptions.
1166
+ var already_seen = false;
1167
+ var prev_dep = active_reaction === null ? null : active_reaction.d;
1168
+ while (prev_dep !== null) {
1169
+ if (prev_dep.t === tracked) {
1170
+ already_seen = true;
1171
+ break;
1172
+ }
1173
+ prev_dep = prev_dep.n;
1174
+ }
1175
+
1176
+ if (!already_seen) {
1177
+ var owner = tracked.b;
1178
+ /** @type {Block | null} */
1179
+ var node = active_block;
1180
+ while (node !== null && node !== owner) {
1181
+ node = node.p;
1182
+ }
1183
+ if (node === null) {
1184
+ disable_scoped_flush = true;
1185
+ }
1186
+ }
1187
+ }
1188
+
1137
1189
  var dependency = active_dependency;
1138
1190
 
1139
1191
  if (dependency === null) {