ripple 0.4.1 → 0.4.2

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 (32) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/package.json +2 -2
  3. package/src/constants.js +4 -0
  4. package/src/runtime/internal/client/constants.js +6 -0
  5. package/src/runtime/internal/client/for.js +224 -50
  6. package/src/runtime/internal/client/hydration.js +0 -16
  7. package/src/runtime/internal/client/index.js +2 -2
  8. package/src/runtime/internal/client/operations.js +1 -2
  9. package/src/runtime/internal/client/render.js +37 -0
  10. package/src/runtime/internal/client/runtime.js +45 -2
  11. package/src/runtime/internal/server/index.js +7 -4
  12. package/tests/client/compiler/compiler.basic.test.tsrx +2 -2
  13. package/tests/client/for-item.test.tsrx +404 -0
  14. package/tests/hydration/compiled/client/basic.js +29 -55
  15. package/tests/hydration/compiled/client/events.js +80 -67
  16. package/tests/hydration/compiled/client/for.js +95 -80
  17. package/tests/hydration/compiled/client/fragment-cursor.js +412 -344
  18. package/tests/hydration/compiled/client/head.js +4 -7
  19. package/tests/hydration/compiled/client/html.js +40 -64
  20. package/tests/hydration/compiled/client/if-children.js +30 -34
  21. package/tests/hydration/compiled/client/if-fragment-controlflow.js +7 -7
  22. package/tests/hydration/compiled/client/if.js +4 -4
  23. package/tests/hydration/compiled/client/mixed-control-flow.js +20 -50
  24. package/tests/hydration/compiled/client/nested-control-flow.js +97 -185
  25. package/tests/hydration/compiled/client/portal.js +2 -2
  26. package/tests/hydration/compiled/client/reactivity.js +17 -31
  27. package/tests/hydration/compiled/client/streaming.js +4 -8
  28. package/tests/hydration/compiled/client/typed-text.js +4 -15
  29. package/tests/hydration/compiled/server/events.js +5 -53
  30. package/tests/hydration/compiled/server/for.js +1 -9
  31. package/tests/hydration/compiled/server/fragment-cursor.js +36 -324
  32. package/tests/server/basic.test.tsrx +23 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # ripple
2
2
 
3
+ ## 0.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1478](https://github.com/Ripple-TS/ripple/pull/1478)
8
+ [`1032539`](https://github.com/Ripple-TS/ripple/commit/10325392e4b1ec7fba78b8ddaf57bc5376adaab1)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Faster buffered server
10
+ rendering. Static text and attribute values are emitted with characters above
11
+ U+00FF as numeric character references (raw-text elements such as `<script>`
12
+ excluded), so a server response stays a one-byte string whenever its dynamic
13
+ data is Latin-1 too: a single static em dash no longer widens the whole body to
14
+ two bytes per character, which halved the flatten, byte-length and UTF-8 encode
15
+ cost of a page. The runtime text escaper checks for `&` and `<` with two
16
+ single-character searches instead of a regex test.
17
+
18
+ - [#1481](https://github.com/Ripple-TS/ripple/pull/1481)
19
+ [`e9382ed`](https://github.com/Ripple-TS/ripple/commit/e9382ed7538779aeb8671b8e3f3fdfd3a897b85a)
20
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Cheaper text-only elements
21
+ and typed loop items. An element whose only child is a typed text expression
22
+ (`<td>{row.name}</td>`) is cloned without a placeholder text node and written
23
+ through the element (`set_text_content`): the first write creates the text node
24
+ with `textContent`, later writes update it in place, and hydration adopts the
25
+ server-rendered text with no cursor descent, which removes a DOM read and a text
26
+ node per cell from every list row. A keyed `@for` key callback reads the item
27
+ directly instead of unwrapping it. Type inference now follows a binding's
28
+ declared type through property writes (`items.value = next` no longer hides
29
+ `track<Row[]>()` from the loop item) and through array and tuple indexing
30
+ (`row.queries[0].elapsed`), so those reads lower to direct text updates in one
31
+ render block per row.
32
+
33
+ - [#1483](https://github.com/Ripple-TS/ripple/pull/1483)
34
+ [`5de359e`](https://github.com/Ripple-TS/ripple/commit/5de359e430391b0e1609795b42d26161fd5c2ff9)
35
+ Thanks [@leonidaz](https://github.com/leonidaz)! - perf: a list item without a
36
+ tracked of its own (a plain list's item, or a local keyed item) is created
37
+ through a lean first run (`run_branch`) instead of the generic block runner:
38
+ fewer checks and no re-run bookkeeping per item.
39
+
40
+ - [#1482](https://github.com/Ripple-TS/ripple/pull/1482)
41
+ [`0030be0`](https://github.com/Ripple-TS/ripple/commit/0030be0aa3c7f1d2ce40500dc57ea980b69c69ec)
42
+ Thanks [@leonidaz](https://github.com/leonidaz)! - perf: cheaper keyed lists. A
43
+ `@for` or `@if` that trails template siblings appends into its parent element
44
+ instead of inserting before a `<!>` placeholder. A list item whose updates live
45
+ in one render block carries that block on the item block itself (`_$_.item`),
46
+ and a keyed item that only its render block reads is held as it is instead of in
47
+ a tracked, re-running its block directly when the key's item is replaced. Leaf
48
+ text is written without a DOM read, from the value the render block last wrote.
49
+ A keyed diff that re-lays most of its items walks the range front to back
50
+ instead of appending every item at the end, about half the reorder cost in
51
+ Chromium and far less right after a layout flush.
52
+ - Updated dependencies
53
+ [[`1032539`](https://github.com/Ripple-TS/ripple/commit/10325392e4b1ec7fba78b8ddaf57bc5376adaab1),
54
+ [`e9382ed`](https://github.com/Ripple-TS/ripple/commit/e9382ed7538779aeb8671b8e3f3fdfd3a897b85a),
55
+ [`0030be0`](https://github.com/Ripple-TS/ripple/commit/0030be0aa3c7f1d2ce40500dc57ea980b69c69ec)]:
56
+ - @tsrx/ripple@0.2.1
57
+
3
58
  ## 0.4.1
4
59
 
5
60
  ### 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.4.1",
6
+ "version": "0.4.2",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -81,7 +81,7 @@
81
81
  "devalue": "^5.9.2",
82
82
  "esm-env": "^1.2.2",
83
83
  "@tsrx/core": "^0.2.0",
84
- "@tsrx/ripple": "0.2.0"
84
+ "@tsrx/ripple": "0.2.1"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@types/estree": "^1.0.8",
package/src/constants.js CHANGED
@@ -6,6 +6,10 @@ export const IS_INDEXED = 1 << 3;
6
6
  // `__anchor` (sibling semantics, no `<!>` wrapper). Distinct from IS_CONTROLLED,
7
7
  // which renders inside a parent element.
8
8
  export const ROOT_CONTROLLED = 1 << 4;
9
+ // A keyed @for whose item is read only by the render block its item block
10
+ // carries: items are held as they are, not in a tracked, and a replaced item
11
+ // re-runs its block directly (see the runtime's `item`).
12
+ export const LOCAL_ITEMS = 1 << 7;
9
13
  export const TEMPLATE_SVG_NAMESPACE = 1 << 5;
10
14
  export const TEMPLATE_MATHML_NAMESPACE = 1 << 6;
11
15
 
@@ -29,6 +29,12 @@ export var SELECTOR = 1 << 23;
29
29
  export var RELEASED = 1 << 24;
30
30
  /** A block whose latest run created deriveds that are recorded for release (see `release_deriveds`). */
31
31
  export var CREATES_DERIVEDS = 1 << 25;
32
+ /**
33
+ * A list item block that carries its body's render block itself (see the
34
+ * runtime's `item`): a branch block for its DOM range, and a reaction that
35
+ * re-runs the body's update function.
36
+ */
37
+ export var ITEM_BLOCK = 1 << 26;
32
38
 
33
39
  export var CONTROL_FLOW_BLOCK = FOR_BLOCK | IF_BLOCK | SWITCH_BLOCK | TRY_BLOCK | COMPOSITE_BLOCK;
34
40
 
@@ -1,8 +1,9 @@
1
1
  /** @import { AppendIntoAnchor, Block, Tracked } from '#client' */
2
2
 
3
- import { IS_CONTROLLED, IS_INDEXED, ROOT_CONTROLLED } from '../../../constants.js';
3
+ import { IS_CONTROLLED, IS_INDEXED, LOCAL_ITEMS, ROOT_CONTROLLED } from '../../../constants.js';
4
4
  import {
5
5
  block as create_block,
6
+ create_block as allocate_block,
6
7
  branch,
7
8
  destroy_block,
8
9
  destroy_block_children,
@@ -11,11 +12,18 @@ import {
11
12
  own_anchor,
12
13
  render,
13
14
  } from './blocks.js';
14
- import { BRANCH_BLOCK, FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
15
+ import {
16
+ BLOCK_HAS_RUN,
17
+ BRANCH_BLOCK,
18
+ DESTROYED,
19
+ FOR_BLOCK,
20
+ ITEM_BLOCK,
21
+ TRACKED_ARRAY,
22
+ } from './constants.js';
15
23
  import { hydrate_first_child, hydrate_next, hydrate_node, hydrating } from './hydration.js';
16
24
  import { get_last_child, next_sibling, resolve_anchor } from './operations.js';
17
25
  import { append } from './template.js';
18
- import { active_block, set, set_tracking, tracked } from './runtime.js';
26
+ import { active_block, run_block, run_branch, set, set_tracking, tracked } from './runtime.js';
19
27
  import { array_from, is_array } from '@tsrx/core/runtime/language-helpers';
20
28
 
21
29
  /**
@@ -42,8 +50,21 @@ function create_item(anchor, value, index, render_fn, is_indexed, is_keyed, map_
42
50
  v: tracked_value,
43
51
  // The item as the collection holds it; a keyed diff compares against it.
44
52
  r: value,
53
+ // The render state of a body that carries its render block on the item
54
+ // block itself (see `item`), or null.
55
+ p: null,
45
56
  };
46
57
 
58
+ // An item without a tracked of its own (a plain list's, or a local one)
59
+ // renders in place: allocated, linked and run through `run_branch`, with
60
+ // none of the first-run bookkeeping a block that may re-run needs.
61
+ if (tracked_value === value && tracked_index === undefined) {
62
+ var lean = allocate_block(BRANCH_BLOCK, run_item, state);
63
+ run_branch(lean, render_fn, anchor, value);
64
+ lean.f ^= BLOCK_HAS_RUN;
65
+ return lean;
66
+ }
67
+
47
68
  // Passed through module state rather than a per-item closure; run_item
48
69
  // reads them before rendering, so nested loops cannot observe a stale pair.
49
70
  item_anchor = anchor;
@@ -81,9 +102,19 @@ var item_anchor = null;
81
102
  var item_render_fn = null;
82
103
 
83
104
  /**
84
- * @param {{ i: Tracked | undefined, v: any }} state
105
+ * @param {{ i: Tracked | undefined, v: any, p: any }} state
85
106
  */
86
107
  function run_item(state) {
108
+ var p = state.p;
109
+ if (p !== null) {
110
+ // A re-run of an item that carries its render block: only the body's
111
+ // update function, tracked (see `item`).
112
+ set_tracking(true);
113
+ /** @type {(prev: any) => void} */ (
114
+ /** @type {ListState} */ (/** @type {Block} */ (/** @type {Block} */ (active_block).p).s).u
115
+ )(p);
116
+ return;
117
+ }
87
118
  var render_fn = /** @type {(anchor: Node, value: any, index?: any) => Block} */ (item_render_fn);
88
119
  var anchor = /** @type {Node} */ (item_anchor);
89
120
  item_render_fn = null;
@@ -91,6 +122,27 @@ function run_item(state) {
91
122
  render_fn(anchor, state.v, state.i);
92
123
  }
93
124
 
125
+ /**
126
+ * The compiled body of a list item whose updates all live in one render block
127
+ * calls this where it would have created that block: the item block carries
128
+ * the render block instead. The update function is the list's (passed to
129
+ * `for`/`for_keyed`, the same for every item) and `prev` its state; it runs
130
+ * tracked here, so its dependencies are the item block's own, and a change
131
+ * re-runs it alone through `run_item`. One block, one run, and one state
132
+ * object less per item.
133
+ * @param {any} prev
134
+ */
135
+ export function item(prev) {
136
+ var block = /** @type {Block} */ (active_block);
137
+ block.s.p = prev;
138
+ block.f |= ITEM_BLOCK;
139
+ set_tracking(true);
140
+ /** @type {(prev: any) => void} */ (
141
+ /** @type {ListState} */ (/** @type {Block} */ (block.p).s).u
142
+ )(prev);
143
+ set_tracking(false);
144
+ }
145
+
94
146
  /**
95
147
  * @param {ListAnchor} anchor
96
148
  * @param {(anchor: Node) => void} render_empty
@@ -236,7 +288,10 @@ class ListState {
236
288
  * @param {boolean} is_indexed
237
289
  * @param {((item: any) => any) | undefined} get_key
238
290
  * @param {((anchor: Node) => void) | undefined} render_empty
239
- * @param {((item: any) => any) | undefined} [map_item]
291
+ * @param {((item: any) => any) | undefined} map_item
292
+ * @param {((prev: any) => void) | undefined} update_fn the update function
293
+ * of an item body that carries its render block (see `item`)
294
+ * @param {boolean} is_local items are held as they are (`LOCAL_ITEMS`)
240
295
  */
241
296
  constructor(
242
297
  anchor,
@@ -247,6 +302,8 @@ class ListState {
247
302
  get_key,
248
303
  render_empty,
249
304
  map_item,
305
+ update_fn,
306
+ is_local,
250
307
  ) {
251
308
  this.a = anchor;
252
309
  this.g = get_collection;
@@ -256,6 +313,8 @@ class ListState {
256
313
  this.k = get_key;
257
314
  this.e = render_empty;
258
315
  this.m = map_item;
316
+ this.u = update_fn;
317
+ this.l = is_local;
259
318
  }
260
319
  }
261
320
 
@@ -287,7 +346,18 @@ function run_for_keyed(state) {
287
346
  var array = collection_to_array(state.g());
288
347
 
289
348
  set_tracking(false);
290
- reconcile_by_key(state.a, block, array, state.r, state.c, state.x, state.k, state.e, state.m);
349
+ reconcile_by_key(
350
+ state.a,
351
+ block,
352
+ array,
353
+ state.r,
354
+ state.c,
355
+ state.x,
356
+ state.k,
357
+ state.e,
358
+ state.m,
359
+ state.l,
360
+ );
291
361
  set_tracking(true);
292
362
 
293
363
  if (hydrating) {
@@ -302,9 +372,11 @@ function run_for_keyed(state) {
302
372
  * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
303
373
  * @param {number} flags
304
374
  * @param {(anchor: Node) => void} [render_empty]
375
+ * @param {(prev: any) => void} [update_fn] for an item body that carries its
376
+ * render block: its update function (see `item`)
305
377
  * @returns {void}
306
378
  */
307
- export function for_block(node, get_collection, render_fn, flags, render_empty) {
379
+ export function for_block(node, get_collection, render_fn, flags, render_empty, update_fn) {
308
380
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
309
381
  var is_indexed = (flags & IS_INDEXED) !== 0;
310
382
  var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
@@ -340,6 +412,9 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
340
412
  is_indexed,
341
413
  undefined,
342
414
  render_empty,
415
+ undefined,
416
+ update_fn,
417
+ false,
343
418
  ),
344
419
  FOR_BLOCK,
345
420
  );
@@ -365,6 +440,8 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
365
440
  * @param {(item: V) => any} [map_item] for a destructuring pattern with a rest
366
441
  * element or a default: destructures the item once, natively, into an object
367
442
  * of the pattern's names, which is what the item's tracked then holds
443
+ * @param {(prev: any) => void} [update_fn] for an item body that carries its
444
+ * render block: its update function (see `item`)
368
445
  * @returns {void}
369
446
  */
370
447
  export function for_block_keyed(
@@ -375,6 +452,7 @@ export function for_block_keyed(
375
452
  get_key,
376
453
  render_empty,
377
454
  map_item,
455
+ update_fn,
378
456
  ) {
379
457
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
380
458
  var is_indexed = (flags & IS_INDEXED) !== 0;
@@ -412,6 +490,8 @@ export function for_block_keyed(
412
490
  get_key,
413
491
  render_empty,
414
492
  map_item,
493
+ update_fn,
494
+ (flags & LOCAL_ITEMS) !== 0,
415
495
  ),
416
496
  FOR_BLOCK,
417
497
  );
@@ -459,13 +539,23 @@ function update_index(block, index) {
459
539
  * @param {Block} block
460
540
  * @param {any} value
461
541
  * @param {((item: any) => any) | undefined} map_item
542
+ * @param {boolean} is_local
462
543
  * @returns {void}
463
544
  */
464
- function update_value(block, value, map_item) {
545
+ function update_value(block, value, map_item, is_local) {
465
546
  var state = block.s;
466
547
  if (state.r !== value) {
467
548
  state.r = value;
468
- set(state.v, map_item === undefined ? value : map_item(value));
549
+ var next = map_item === undefined ? value : map_item(value);
550
+ if (is_local) {
551
+ // Held as it is, read by the item block's update function alone
552
+ // (`LOCAL_ITEMS`) off its state's `$item` slot: the block re-runs here.
553
+ state.v = next;
554
+ state.p.$item = next;
555
+ run_block(block);
556
+ } else {
557
+ set(state.v, next);
558
+ }
469
559
  }
470
560
  }
471
561
 
@@ -481,6 +571,7 @@ function update_value(block, value, map_item) {
481
571
  * @param {((item: V) => K) | undefined} get_key identity keys when omitted
482
572
  * @param {((anchor: Node) => void) | undefined} render_empty
483
573
  * @param {((item: V) => any) | undefined} map_item
574
+ * @param {boolean} is_local items are held as they are (`LOCAL_ITEMS`)
484
575
  * @returns {void}
485
576
  *
486
577
  * The first run only creates items, so it lives in this small function and
@@ -496,6 +587,7 @@ function reconcile_by_key(
496
587
  get_key,
497
588
  render_empty,
498
589
  map_item,
590
+ is_local,
499
591
  ) {
500
592
  var b_length = b.length;
501
593
  var state = /** @type {ListState} */ (block.s);
@@ -512,7 +604,7 @@ function reconcile_by_key(
512
604
  if (get_key !== undefined) {
513
605
  b_keys[j] = get_key(value);
514
606
  }
515
- b_blocks[j] = create_item(anchor, value, j, render_fn, is_indexed, true, map_item);
607
+ b_blocks[j] = create_item(anchor, value, j, render_fn, is_indexed, !is_local, map_item);
516
608
  }
517
609
 
518
610
  state.array = b;
@@ -531,6 +623,7 @@ function reconcile_by_key(
531
623
  get_key,
532
624
  render_empty,
533
625
  map_item,
626
+ is_local,
534
627
  );
535
628
  }
536
629
 
@@ -547,6 +640,7 @@ function reconcile_by_key(
547
640
  * @param {((item: V) => K) | undefined} get_key
548
641
  * @param {((anchor: Node) => void) | undefined} render_empty
549
642
  * @param {((item: V) => any) | undefined} map_item
643
+ * @param {boolean} is_local items are held as they are (`LOCAL_ITEMS`)
550
644
  * @returns {void}
551
645
  */
552
646
  function reconcile_by_key_diff(
@@ -559,7 +653,9 @@ function reconcile_by_key_diff(
559
653
  get_key,
560
654
  render_empty,
561
655
  map_item,
656
+ is_local,
562
657
  ) {
658
+ var is_keyed = !is_local;
563
659
  var state = /** @type {ListState} */ (block.s);
564
660
 
565
661
  // Variables used in conditional branches - declare with initial values
@@ -627,7 +723,7 @@ function reconcile_by_key_diff(
627
723
  // Fast-path for create
628
724
  if (a_length === 0) {
629
725
  for (; j < b_length; j++) {
630
- b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, true, map_item);
726
+ b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, is_keyed, map_item);
631
727
  }
632
728
  state.array = b;
633
729
  state.blocks = b_blocks;
@@ -675,7 +771,7 @@ function reconcile_by_key_diff(
675
771
  update_index(b_block, b_start);
676
772
  }
677
773
  if (get_key !== undefined) {
678
- update_value(b_block, b_val, map_item);
774
+ update_value(b_block, b_val, map_item, is_local);
679
775
  }
680
776
  a_start++;
681
777
  b_start++;
@@ -688,7 +784,7 @@ function reconcile_by_key_diff(
688
784
  update_index(b_block, b_end);
689
785
  }
690
786
  if (get_key !== undefined) {
691
- update_value(b_block, b_val, map_item);
787
+ update_value(b_block, b_val, map_item, is_local);
692
788
  }
693
789
  a_end--;
694
790
  b_end--;
@@ -710,7 +806,7 @@ function reconcile_by_key_diff(
710
806
  update_index(b_block, b_start);
711
807
  }
712
808
  if (get_key !== undefined) {
713
- update_value(b_block, b_val, map_item);
809
+ update_value(b_block, b_val, map_item, is_local);
714
810
  }
715
811
  move(b_block, /** @type {ChildNode} */ (a_blocks[a_start].s.start));
716
812
  a_end--;
@@ -725,7 +821,7 @@ function reconcile_by_key_diff(
725
821
  update_index(b_block, b_end);
726
822
  }
727
823
  if (get_key !== undefined) {
728
- update_value(b_block, b_val, map_item);
824
+ update_value(b_block, b_val, map_item, is_local);
729
825
  }
730
826
  move(b_block, block_start(b_blocks, b_end + 1, b_length, anchor));
731
827
  a_start++;
@@ -747,7 +843,7 @@ function reconcile_by_key_diff(
747
843
  b_start,
748
844
  render_fn,
749
845
  is_indexed,
750
- true,
846
+ is_keyed,
751
847
  map_item,
752
848
  );
753
849
  b_start++;
@@ -796,7 +892,7 @@ function reconcile_by_key_diff(
796
892
  update_index(b_block, j);
797
893
  }
798
894
  if (get_key !== undefined) {
799
- update_value(b_block, b_val, map_item);
895
+ update_value(b_block, b_val, map_item, is_local);
800
896
  }
801
897
  ++patched;
802
898
  break;
@@ -839,7 +935,7 @@ function reconcile_by_key_diff(
839
935
  update_index(b_block, j);
840
936
  }
841
937
  if (get_key !== undefined) {
842
- update_value(b_block, b_val, map_item);
938
+ update_value(b_block, b_val, map_item, is_local);
843
939
  }
844
940
  ++patched;
845
941
  } else if (!fast_path_removal) {
@@ -864,6 +960,7 @@ function reconcile_by_key_diff(
864
960
  get_key,
865
961
  render_empty,
866
962
  map_item,
963
+ is_local,
867
964
  );
868
965
  return;
869
966
  } else if (moved) {
@@ -872,26 +969,23 @@ function reconcile_by_key_diff(
872
969
  j = seq.length - 1;
873
970
 
874
971
  // When most surviving items have to move anyway, re-lay the whole range
875
- // in order before a fixed target: inserting before the same node is
876
- // noticeably cheaper per item than moving into arbitrary positions.
972
+ // in order (see `relay`).
877
973
  if ((patched - seq.length) * 3 > b_left * 2) {
878
- var relay_target = block_start(b_blocks, b_end + 1, b_length, anchor);
879
- for (i = 0; i < b_left; i++) {
880
- pos = i + b_start;
881
- if (sources[i] === 0) {
882
- b_blocks[pos] = create_item(
883
- relay_target,
884
- b[pos],
885
- pos,
886
- render_fn,
887
- is_indexed,
888
- true,
889
- map_item,
890
- );
891
- } else {
892
- move(b_blocks[pos], relay_target);
893
- }
894
- }
974
+ relay(
975
+ anchor,
976
+ a_blocks,
977
+ a_start,
978
+ a_end,
979
+ b,
980
+ b_blocks,
981
+ b_start,
982
+ b_end,
983
+ sources,
984
+ render_fn,
985
+ is_indexed,
986
+ is_keyed,
987
+ map_item,
988
+ );
895
989
  state.array = b;
896
990
  state.blocks = b_blocks;
897
991
  state.keys = b_keys;
@@ -905,7 +999,7 @@ function reconcile_by_key_diff(
905
999
  next_pos = pos + 1;
906
1000
 
907
1001
  var target = block_start(b_blocks, next_pos, b_length, anchor);
908
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true, map_item);
1002
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, is_keyed, map_item);
909
1003
  } else if (j < 0 || i !== seq[j]) {
910
1004
  pos = i + b_start;
911
1005
  next_pos = pos + 1;
@@ -924,7 +1018,7 @@ function reconcile_by_key_diff(
924
1018
  next_pos = pos + 1;
925
1019
 
926
1020
  var target = block_start(b_blocks, next_pos, b_length, anchor);
927
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true, map_item);
1021
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, is_keyed, map_item);
928
1022
  }
929
1023
  }
930
1024
  }
@@ -934,6 +1028,81 @@ function reconcile_by_key_diff(
934
1028
  state.keys = b_keys;
935
1029
  }
936
1030
 
1031
+ /**
1032
+ * Re-lays the unmatched range `b_start..b_end` in its new order, walking it
1033
+ * front to back: `cursor` is the first node of the range that is not in place
1034
+ * yet, so a block already there costs a compare and a sibling read, and every
1035
+ * other block (or a new item) is inserted before `cursor`. Inserting each item
1036
+ * toward the front is what the reverse fast path does too; appending every
1037
+ * item at the end of the range instead cost about twice as much in Blink's
1038
+ * reorder floor (500 rows: 0.55 ms against 0.29), and several times more
1039
+ * right after a layout flush, so this holds even when most items move.
1040
+ * @template V
1041
+ * @param {ListAnchor} anchor
1042
+ * @param {Block[]} a_blocks the old blocks; destroyed ones are skipped
1043
+ * @param {number} a_start
1044
+ * @param {number} a_end
1045
+ * @param {V[]} b
1046
+ * @param {Block[]} b_blocks matched blocks at their new index, holes for new items
1047
+ * @param {number} b_start
1048
+ * @param {number} b_end
1049
+ * @param {Int32Array} sources zero at the index of a new item
1050
+ * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
1051
+ * @param {boolean} is_indexed
1052
+ * @param {boolean} is_keyed
1053
+ * @param {((item: V) => any) | undefined} map_item
1054
+ */
1055
+ function relay(
1056
+ anchor,
1057
+ a_blocks,
1058
+ a_start,
1059
+ a_end,
1060
+ b,
1061
+ b_blocks,
1062
+ b_start,
1063
+ b_end,
1064
+ sources,
1065
+ render_fn,
1066
+ is_indexed,
1067
+ is_keyed,
1068
+ map_item,
1069
+ ) {
1070
+ var b_length = b.length;
1071
+ var end_target = block_start(b_blocks, b_end + 1, b_length, anchor);
1072
+ /** @type {ChildNode | AppendIntoAnchor | null} */
1073
+ var cursor = null;
1074
+
1075
+ // The range starts at the first surviving old block.
1076
+ for (var i = a_start; i <= a_end && cursor === null; i++) {
1077
+ var old_block = a_blocks[i];
1078
+ if ((old_block.f & DESTROYED) === 0) {
1079
+ cursor = /** @type {ChildNode | null} */ (get_first_node(old_block));
1080
+ }
1081
+ }
1082
+ if (cursor === null) {
1083
+ cursor = end_target;
1084
+ }
1085
+
1086
+ for (var pos = b_start; pos <= b_end; pos++) {
1087
+ if (sources[pos - b_start] === 0) {
1088
+ b_blocks[pos] = create_item(cursor, b[pos], pos, render_fn, is_indexed, is_keyed, map_item);
1089
+ continue;
1090
+ }
1091
+ var block = b_blocks[pos];
1092
+ var first = get_first_node(block);
1093
+ if (first === null) {
1094
+ // Renders nothing: it has no place to take.
1095
+ continue;
1096
+ }
1097
+ if (first !== cursor) {
1098
+ move(block, cursor);
1099
+ continue;
1100
+ }
1101
+ var after = next_sibling(/** @type {Node} */ (get_last_node(block)));
1102
+ cursor = after === null ? end_target : /** @type {ChildNode} */ (after);
1103
+ }
1104
+ }
1105
+
937
1106
  /**
938
1107
  * @template V
939
1108
  * @param {ListAnchor} anchor
@@ -1211,18 +1380,23 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1211
1380
  j = seq.length - 1;
1212
1381
 
1213
1382
  // When most surviving items have to move anyway, re-lay the whole range
1214
- // in order before a fixed target: inserting before the same node is
1215
- // noticeably cheaper per item than moving into arbitrary positions.
1383
+ // in order (see `relay`).
1216
1384
  if ((patched - seq.length) * 3 > b_left * 2) {
1217
- var relay_target = block_start(b_blocks, b_end + 1, b_length, anchor);
1218
- for (i = 0; i < b_left; i++) {
1219
- pos = i + b_start;
1220
- if (sources[i] === 0) {
1221
- b_blocks[pos] = create_item(relay_target, b[pos], pos, render_fn, is_indexed, false);
1222
- } else {
1223
- move(b_blocks[pos], relay_target);
1224
- }
1225
- }
1385
+ relay(
1386
+ anchor,
1387
+ a_blocks,
1388
+ a_start,
1389
+ a_end,
1390
+ b,
1391
+ b_blocks,
1392
+ b_start,
1393
+ b_end,
1394
+ sources,
1395
+ render_fn,
1396
+ is_indexed,
1397
+ false,
1398
+ undefined,
1399
+ );
1226
1400
  state.array = b;
1227
1401
  state.blocks = b_blocks;
1228
1402
  return;
@@ -76,22 +76,6 @@ export function hydrate_first_child(is_text) {
76
76
  return (hydrate_node = child);
77
77
  }
78
78
 
79
- /**
80
- * Adopts the text node that is an element's only child, leaving the cursor on
81
- * the element: nothing else inside it navigates, so the parent's sibling
82
- * traversal continues from the element with no pop().
83
- * @returns {Node}
84
- */
85
- export function hydrate_text_child() {
86
- var child = first_child_getter.call(/** @type {Node} */ (hydrate_node));
87
-
88
- if (child === null || child.nodeType !== TEXT_NODE) {
89
- child = repair_first_child(child);
90
- }
91
-
92
- return child;
93
- }
94
-
95
79
  /**
96
80
  * Handles `<p>{text}</p>` where `text` rendered empty on the server: there is
97
81
  * no text node to adopt (or, with siblings, the next node is not one), so one
@@ -4,7 +4,6 @@ export {
4
4
  next_sibling as sibling,
5
5
  hydrate_first_child as hydrate_child,
6
6
  hydrate_next_sibling as hydrate_sibling,
7
- hydrate_text_child as hydrate_text,
8
7
  append_into,
9
8
  document,
10
9
  create_text,
@@ -13,6 +12,7 @@ export {
13
12
 
14
13
  export {
15
14
  set_text,
15
+ set_text_content,
16
16
  set_class,
17
17
  set_style,
18
18
  set_attribute,
@@ -84,7 +84,7 @@ export { composite } from './composite.js';
84
84
 
85
85
  export { render_component } from './component.js';
86
86
 
87
- export { for_block as for, for_block_keyed as for_keyed } from './for.js';
87
+ export { for_block as for, for_block_keyed as for_keyed, item } from './for.js';
88
88
 
89
89
  export { selector, selector_match } from './selector.js';
90
90
 
@@ -4,13 +4,12 @@ import {
4
4
  hydrate_first_child,
5
5
  hydrate_next_sibling,
6
6
  hydrate_node,
7
- hydrate_text_child,
8
7
  hydrating,
9
8
  set_hydrate_node,
10
9
  } from './hydration.js';
11
10
  import { get_descriptor } from '@tsrx/core/runtime/language-helpers';
12
11
 
13
- export { hydrate_first_child, hydrate_next_sibling, hydrate_text_child };
12
+ export { hydrate_first_child, hydrate_next_sibling };
14
13
 
15
14
  /**
16
15
  * The `firstChild` / `nextSibling` getters, called directly so a traversal