ripple 0.4.1 → 0.4.3

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 (96) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/package.json +9 -3
  3. package/src/constants.js +4 -0
  4. package/src/jsx-runtime.d.ts +2 -1
  5. package/src/runtime/array.js +15 -9
  6. package/src/runtime/index-client.js +25 -7
  7. package/src/runtime/index-server.js +1 -0
  8. package/src/runtime/internal/client/attributes.js +313 -0
  9. package/src/runtime/internal/client/blocks.js +4 -14
  10. package/src/runtime/internal/client/component.js +2 -5
  11. package/src/runtime/internal/client/composite.js +14 -6
  12. package/src/runtime/internal/client/constants.js +12 -6
  13. package/src/runtime/internal/client/context.js +3 -2
  14. package/src/runtime/internal/client/css.js +11 -26
  15. package/src/runtime/internal/client/errors.js +188 -0
  16. package/src/runtime/internal/client/events.js +79 -34
  17. package/src/runtime/internal/client/expression.js +21 -116
  18. package/src/runtime/internal/client/for.js +272 -222
  19. package/src/runtime/internal/client/head.js +4 -3
  20. package/src/runtime/internal/client/html.js +5 -3
  21. package/src/runtime/internal/client/hydrate.js +484 -0
  22. package/src/runtime/internal/client/hydration-enabled.js +7 -0
  23. package/src/runtime/internal/client/hydration.js +16 -97
  24. package/src/runtime/internal/client/if.js +13 -9
  25. package/src/runtime/internal/client/index.js +16 -8
  26. package/src/runtime/internal/client/operations.js +12 -9
  27. package/src/runtime/internal/client/portal.js +3 -2
  28. package/src/runtime/internal/client/render.js +75 -321
  29. package/src/runtime/internal/client/root-boundary-enabled.js +7 -0
  30. package/src/runtime/internal/client/runtime.js +129 -362
  31. package/src/runtime/internal/client/template-ns.js +47 -0
  32. package/src/runtime/internal/client/template.js +63 -140
  33. package/src/runtime/internal/client/track-async.js +356 -0
  34. package/src/runtime/internal/client/try.js +133 -319
  35. package/src/runtime/internal/client/types.d.ts +48 -24
  36. package/src/runtime/internal/server/index.js +38 -33
  37. package/src/runtime/internal/server/prerender.js +27 -0
  38. package/src/server/index.js +1 -0
  39. package/src/utils/class-name.js +28 -0
  40. package/tests/client/basic/basic.errors.test.tsrx +3 -3
  41. package/tests/client/compiler/__snapshots__/compiler.typescript.test.tsrx.snap +0 -2
  42. package/tests/client/compiler/compiler.basic.test.tsrx +4 -4
  43. package/tests/client/compiler/compiler.tracked-access.test.tsrx +3 -3
  44. package/tests/client/composite/composite.props.test.tsrx +10 -10
  45. package/tests/client/derived-release.test.tsrx +3 -3
  46. package/tests/client/for-item.test.tsrx +443 -0
  47. package/tests/client/for-selector.test.tsrx +95 -0
  48. package/tests/hydration/compiled/client/basic.js +97 -123
  49. package/tests/hydration/compiled/client/composite.js +5 -5
  50. package/tests/hydration/compiled/client/events.js +84 -79
  51. package/tests/hydration/compiled/client/for.js +229 -233
  52. package/tests/hydration/compiled/client/fragment-cursor.js +781 -481
  53. package/tests/hydration/compiled/client/head.js +23 -26
  54. package/tests/hydration/compiled/client/hmr.js +4 -4
  55. package/tests/hydration/compiled/client/html-in-template.js +5 -5
  56. package/tests/hydration/compiled/client/html.js +174 -208
  57. package/tests/hydration/compiled/client/if-children.js +71 -70
  58. package/tests/hydration/compiled/client/if-fragment-controlflow.js +46 -46
  59. package/tests/hydration/compiled/client/if.js +26 -26
  60. package/tests/hydration/compiled/client/mixed-control-flow.js +63 -105
  61. package/tests/hydration/compiled/client/nested-control-flow.js +170 -390
  62. package/tests/hydration/compiled/client/portal.js +10 -10
  63. package/tests/hydration/compiled/client/reactivity.js +36 -50
  64. package/tests/hydration/compiled/client/return.js +2 -2
  65. package/tests/hydration/compiled/client/streaming.js +34 -40
  66. package/tests/hydration/compiled/client/switch.js +25 -25
  67. package/tests/hydration/compiled/client/track-async-serialization.js +41 -41
  68. package/tests/hydration/compiled/client/try.js +15 -15
  69. package/tests/hydration/compiled/client/typed-text.js +8 -19
  70. package/tests/hydration/compiled/server/basic.js +6 -6
  71. package/tests/hydration/compiled/server/events.js +13 -61
  72. package/tests/hydration/compiled/server/for.js +23 -31
  73. package/tests/hydration/compiled/server/fragment-cursor.js +214 -366
  74. package/tests/hydration/compiled/server/head.js +6 -6
  75. package/tests/hydration/compiled/server/hmr.js +1 -1
  76. package/tests/hydration/compiled/server/html.js +1 -1
  77. package/tests/hydration/compiled/server/if-children.js +9 -9
  78. package/tests/hydration/compiled/server/if.js +5 -5
  79. package/tests/hydration/compiled/server/mixed-control-flow.js +4 -4
  80. package/tests/hydration/compiled/server/portal.js +1 -1
  81. package/tests/hydration/compiled/server/reactivity.js +9 -9
  82. package/tests/hydration/compiled/server/streaming.js +9 -9
  83. package/tests/hydration/compiled/server/switch.js +4 -4
  84. package/tests/hydration/compiled/server/track-async-serialization.js +16 -16
  85. package/tests/hydration/compiled/server/try.js +4 -4
  86. package/tests/hydration/compiled/server/typed-text.js +1 -1
  87. package/tests/hydration/components/fragment-cursor.tsrx +60 -0
  88. package/tests/hydration/components/track-async-serialization.tsrx +2 -2
  89. package/tests/hydration/fragment-cursor.test.js +34 -1
  90. package/tests/hydration/track-async-serialization.test.js +1 -1
  91. package/tests/server/basic.components.test.tsrx +3 -3
  92. package/tests/server/basic.test.tsrx +23 -0
  93. package/tests/server/track-async-serialization.test.tsrx +6 -6
  94. package/tests/utils/tracked-types.test.js +3 -3
  95. package/types/index.d.ts +8 -8
  96. package/types/server.d.ts +17 -0
@@ -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,55 +12,97 @@ 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';
28
+ import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
20
29
 
21
30
  /**
22
31
  * @template V
23
32
  * @param {Node | AppendIntoAnchor} anchor
24
33
  * @param {V} value
25
34
  * @param {number} index
26
- * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
27
- * @param {boolean} is_indexed
28
- * @param {boolean} is_keyed
29
- * @param {((item: V) => any) | undefined} [map_item] a keyed loop's destructuring of the item (see `for_block_keyed`)
35
+ * @param {any} key the key the item was matched by (keyed lists)
36
+ * @param {ListState} state the list's state: how items render
30
37
  * @returns {Block}
31
38
  */
32
- function create_item(anchor, value, index, render_fn, is_indexed, is_keyed, map_item) {
39
+ function create_item(anchor, value, index, key, state) {
33
40
  var block = /** @type {Block} */ (active_block);
34
- var tracked_index = is_indexed ? tracked(index, block) : undefined;
35
- var tracked_value = is_keyed
41
+ var render_fn = state.r;
42
+ var map_item = state.m;
43
+ var tracked_index = state.x ? tracked(index, block) : undefined;
44
+ var tracked_value = state.y
36
45
  ? tracked(map_item === undefined ? value : map_item(value), block)
37
46
  : value;
38
- var state = {
47
+ var item_state = {
39
48
  start: null,
40
49
  end: null,
41
50
  i: tracked_index,
42
51
  v: tracked_value,
43
52
  // The item as the collection holds it; a keyed diff compares against it.
44
53
  r: value,
54
+ // The key the item was matched by: fixed for the block's whole life, so
55
+ // the body reads it as a plain value (a compiled `@if (outer === item.key)`
56
+ // depends on the selector alone, not on the item).
57
+ k: key,
58
+ // The render state of a body that carries its render block on the item
59
+ // block itself (see `item`), or null.
60
+ p: null,
45
61
  };
46
62
 
63
+ // An item without a tracked of its own (a plain list's, or a local one)
64
+ // renders in place: allocated, linked and run through `run_branch`, with
65
+ // none of the first-run bookkeeping a block that may re-run needs.
66
+ if (tracked_value === value && tracked_index === undefined) {
67
+ var lean = allocate_block(BRANCH_BLOCK, run_item, item_state);
68
+ run_branch(lean, render_fn, anchor, value, key);
69
+ lean.f ^= BLOCK_HAS_RUN;
70
+ return lean;
71
+ }
72
+
47
73
  // Passed through module state rather than a per-item closure; run_item
48
74
  // reads them before rendering, so nested loops cannot observe a stale pair.
49
75
  item_anchor = anchor;
50
76
  item_render_fn = render_fn;
51
- var b = create_block(BRANCH_BLOCK, run_item, state);
77
+ var b = create_block(BRANCH_BLOCK, run_item, item_state);
52
78
 
53
79
  // The item's tracked value and index are owned by the item block itself.
54
- if (is_keyed) {
80
+ if (tracked_value !== value) {
55
81
  /** @type {Tracked} */ (tracked_value).b = b;
56
82
  }
57
- if (is_indexed) {
83
+ if (tracked_index !== undefined) {
58
84
  /** @type {Tracked} */ (tracked_index).b = b;
59
85
  }
60
86
  return b;
61
87
  }
62
88
 
89
+ /**
90
+ * A matched item at its new index: its tracked index and, for a computed key,
91
+ * its tracked value follow the collection.
92
+ * @param {ListState} state
93
+ * @param {Block} block
94
+ * @param {any} value
95
+ * @param {number} index
96
+ */
97
+ function patch_item(state, block, value, index) {
98
+ if (state.x) {
99
+ update_index(block, index);
100
+ }
101
+ if (state.k !== undefined) {
102
+ update_value(block, value, state.m, state.l);
103
+ }
104
+ }
105
+
63
106
  /**
64
107
  * The insertion point of a list: a node the items go before, or, for a
65
108
  * controlled list (the sole content of its parent), an append-into sentinel so
@@ -72,23 +115,56 @@ function create_item(anchor, value, index, render_fn, is_indexed, is_keyed, map_
72
115
  * @returns {AppendIntoAnchor}
73
116
  */
74
117
  function controlled_anchor(parent) {
75
- return { parent, into: true };
118
+ return { parent, into: true, tail: false };
76
119
  }
77
120
 
78
121
  /** @type {Node | AppendIntoAnchor | null} */
79
122
  var item_anchor = null;
80
- /** @type {((anchor: Node, value: any, index?: any) => Block) | null} */
123
+ /** @type {((anchor: Node, value: any, index?: any, key?: any) => Block) | null} */
81
124
  var item_render_fn = null;
82
125
 
83
126
  /**
84
- * @param {{ i: Tracked | undefined, v: any }} state
127
+ * @param {{ i: Tracked | undefined, v: any, k: any, p: any }} state
85
128
  */
86
129
  function run_item(state) {
87
- var render_fn = /** @type {(anchor: Node, value: any, index?: any) => Block} */ (item_render_fn);
130
+ var p = state.p;
131
+ if (p !== null) {
132
+ // A re-run of an item that carries its render block: only the body's
133
+ // update function, tracked (see `item`).
134
+ set_tracking(true);
135
+ /** @type {(prev: any) => void} */ (
136
+ /** @type {ListState} */ (/** @type {Block} */ (/** @type {Block} */ (active_block).p).s).u
137
+ )(p);
138
+ return;
139
+ }
140
+ var render_fn = /** @type {(anchor: Node, value: any, index?: any, key?: any) => Block} */ (
141
+ item_render_fn
142
+ );
88
143
  var anchor = /** @type {Node} */ (item_anchor);
89
144
  item_render_fn = null;
90
145
  item_anchor = null;
91
- render_fn(anchor, state.v, state.i);
146
+ render_fn(anchor, state.v, state.i, state.k);
147
+ }
148
+
149
+ /**
150
+ * The compiled body of a list item whose updates all live in one render block
151
+ * calls this where it would have created that block: the item block carries
152
+ * the render block instead. The update function is the list's (passed to
153
+ * `for`/`for_keyed`, the same for every item) and `prev` its state; it runs
154
+ * tracked here, so its dependencies are the item block's own, and a change
155
+ * re-runs it alone through `run_item`. One block, one run, and one state
156
+ * object less per item.
157
+ * @param {any} prev
158
+ */
159
+ export function item(prev) {
160
+ var block = /** @type {Block} */ (active_block);
161
+ block.s.p = prev;
162
+ block.f |= ITEM_BLOCK;
163
+ set_tracking(true);
164
+ /** @type {(prev: any) => void} */ (
165
+ /** @type {ListState} */ (/** @type {Block} */ (block.p).s).u
166
+ )(prev);
167
+ set_tracking(false);
92
168
  }
93
169
 
94
170
  /**
@@ -236,7 +312,11 @@ class ListState {
236
312
  * @param {boolean} is_indexed
237
313
  * @param {((item: any) => any) | undefined} get_key
238
314
  * @param {((anchor: Node) => void) | undefined} render_empty
239
- * @param {((item: any) => any) | undefined} [map_item]
315
+ * @param {((item: any) => any) | undefined} map_item
316
+ * @param {((prev: any) => void) | undefined} update_fn the update function
317
+ * of an item body that carries its render block (see `item`)
318
+ * @param {boolean} is_local items are held as they are (`LOCAL_ITEMS`)
319
+ * @param {boolean} is_keyed each item holds a tracked of its value
240
320
  */
241
321
  constructor(
242
322
  anchor,
@@ -247,6 +327,9 @@ class ListState {
247
327
  get_key,
248
328
  render_empty,
249
329
  map_item,
330
+ update_fn,
331
+ is_local,
332
+ is_keyed,
250
333
  ) {
251
334
  this.a = anchor;
252
335
  this.g = get_collection;
@@ -256,6 +339,9 @@ class ListState {
256
339
  this.k = get_key;
257
340
  this.e = render_empty;
258
341
  this.m = map_item;
342
+ this.u = update_fn;
343
+ this.l = is_local;
344
+ this.y = is_keyed;
259
345
  }
260
346
  }
261
347
 
@@ -268,13 +354,13 @@ function run_for(state) {
268
354
 
269
355
  // Items render untracked, as in a branch; the list tracks only its collection.
270
356
  set_tracking(false);
271
- reconcile_by_ref(state.a, block, array, state.r, state.c, state.x, state.e);
357
+ reconcile_by_ref(state.a, block, array);
272
358
  set_tracking(true);
273
359
 
274
360
  // Re-anchor a hydrated list: the hydrated anchor is the block's start
275
361
  // marker; later inserts and end moves must go before the cursor, which now
276
362
  // sits after the hydrated items.
277
- if (hydrating) {
363
+ if (HYDRATION && hydrating) {
278
364
  state.a = /** @type {Element | Text} */ (hydrate_node);
279
365
  }
280
366
  }
@@ -287,10 +373,10 @@ function run_for_keyed(state) {
287
373
  var array = collection_to_array(state.g());
288
374
 
289
375
  set_tracking(false);
290
- reconcile_by_key(state.a, block, array, state.r, state.c, state.x, state.k, state.e, state.m);
376
+ reconcile_by_key(state.a, block, array);
291
377
  set_tracking(true);
292
378
 
293
- if (hydrating) {
379
+ if (HYDRATION && hydrating) {
294
380
  state.a = /** @type {Element | Text} */ (hydrate_node);
295
381
  }
296
382
  }
@@ -302,9 +388,11 @@ function run_for_keyed(state) {
302
388
  * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
303
389
  * @param {number} flags
304
390
  * @param {(anchor: Node) => void} [render_empty]
391
+ * @param {(prev: any) => void} [update_fn] for an item body that carries its
392
+ * render block: its update function (see `item`)
305
393
  * @returns {void}
306
394
  */
307
- export function for_block(node, get_collection, render_fn, flags, render_empty) {
395
+ export function for_block(node, get_collection, render_fn, flags, render_empty, update_fn) {
308
396
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
309
397
  var is_indexed = (flags & IS_INDEXED) !== 0;
310
398
  var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
@@ -315,7 +403,7 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
315
403
  var boundary;
316
404
 
317
405
  if (is_controlled) {
318
- if (hydrating) {
406
+ if (HYDRATION && hydrating) {
319
407
  // The cursor sits on the list's element; the items are its children.
320
408
  hydrate_first_child();
321
409
  } else {
@@ -323,7 +411,7 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
323
411
  }
324
412
  }
325
413
 
326
- if (hydrating) {
414
+ if (HYDRATION && hydrating) {
327
415
  if (root_controlled) {
328
416
  boundary = /** @type {Node} */ (hydrate_node);
329
417
  }
@@ -340,13 +428,17 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
340
428
  is_indexed,
341
429
  undefined,
342
430
  render_empty,
431
+ undefined,
432
+ update_fn,
433
+ false,
434
+ false,
343
435
  ),
344
436
  FOR_BLOCK,
345
437
  );
346
438
 
347
439
  if (!is_controlled) own_anchor(node, /** @type {Node} */ (anchor));
348
440
 
349
- if (hydrating && root_controlled) {
441
+ if (HYDRATION && hydrating && root_controlled) {
350
442
  // The original `node`: for a sentinel, `hydrate_append` performs the
351
443
  // cursor advance that stands in for the eliminated sibling navigation.
352
444
  append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
@@ -365,6 +457,8 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
365
457
  * @param {(item: V) => any} [map_item] for a destructuring pattern with a rest
366
458
  * element or a default: destructures the item once, natively, into an object
367
459
  * of the pattern's names, which is what the item's tracked then holds
460
+ * @param {(prev: any) => void} [update_fn] for an item body that carries its
461
+ * render block: its update function (see `item`)
368
462
  * @returns {void}
369
463
  */
370
464
  export function for_block_keyed(
@@ -375,6 +469,7 @@ export function for_block_keyed(
375
469
  get_key,
376
470
  render_empty,
377
471
  map_item,
472
+ update_fn,
378
473
  ) {
379
474
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
380
475
  var is_indexed = (flags & IS_INDEXED) !== 0;
@@ -386,7 +481,7 @@ export function for_block_keyed(
386
481
  if (is_controlled) {
387
482
  var parent_node = /** @type {Element} */ (node);
388
483
 
389
- if (hydrating) {
484
+ if (HYDRATION && hydrating) {
390
485
  hydrate_first_child();
391
486
  anchor = /** @type {Element | Text} */ (get_last_child(parent_node));
392
487
  } else {
@@ -394,7 +489,7 @@ export function for_block_keyed(
394
489
  }
395
490
  }
396
491
 
397
- if (hydrating) {
492
+ if (HYDRATION && hydrating) {
398
493
  if (root_controlled) {
399
494
  boundary = /** @type {Node} */ (hydrate_node);
400
495
  }
@@ -412,13 +507,16 @@ export function for_block_keyed(
412
507
  get_key,
413
508
  render_empty,
414
509
  map_item,
510
+ update_fn,
511
+ (flags & LOCAL_ITEMS) !== 0,
512
+ (flags & LOCAL_ITEMS) === 0,
415
513
  ),
416
514
  FOR_BLOCK,
417
515
  );
418
516
 
419
517
  if (!is_controlled) own_anchor(node, /** @type {Node} */ (anchor));
420
518
 
421
- if (hydrating && root_controlled) {
519
+ if (HYDRATION && hydrating && root_controlled) {
422
520
  // The original `node`: for a sentinel, `hydrate_append` performs the
423
521
  // cursor advance that stands in for the eliminated sibling navigation.
424
522
  append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
@@ -459,46 +557,40 @@ function update_index(block, index) {
459
557
  * @param {Block} block
460
558
  * @param {any} value
461
559
  * @param {((item: any) => any) | undefined} map_item
560
+ * @param {boolean} is_local
462
561
  * @returns {void}
463
562
  */
464
- function update_value(block, value, map_item) {
563
+ function update_value(block, value, map_item, is_local) {
465
564
  var state = block.s;
466
565
  if (state.r !== value) {
467
566
  state.r = value;
468
- set(state.v, map_item === undefined ? value : map_item(value));
567
+ var next = map_item === undefined ? value : map_item(value);
568
+ if (is_local) {
569
+ // Held as it is, read by the item block's update function alone
570
+ // (`LOCAL_ITEMS`) off its state's `$item` slot: the block re-runs here.
571
+ state.v = next;
572
+ state.p.$item = next;
573
+ run_block(block);
574
+ } else {
575
+ set(state.v, next);
576
+ }
469
577
  }
470
578
  }
471
579
 
472
580
  /**
473
581
  * @template V
474
- * @template K
475
582
  * @param {ListAnchor} anchor
476
583
  * @param {Block} block
477
584
  * @param {V[]} b
478
- * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
479
- * @param {boolean} is_controlled
480
- * @param {boolean} is_indexed
481
- * @param {((item: V) => K) | undefined} get_key identity keys when omitted
482
- * @param {((anchor: Node) => void) | undefined} render_empty
483
- * @param {((item: V) => any) | undefined} map_item
484
585
  * @returns {void}
485
586
  *
486
587
  * The first run only creates items, so it lives in this small function and
487
588
  * the diff below is compiled only once a list actually changes.
488
589
  */
489
- function reconcile_by_key(
490
- anchor,
491
- block,
492
- b,
493
- render_fn,
494
- is_controlled,
495
- is_indexed,
496
- get_key,
497
- render_empty,
498
- map_item,
499
- ) {
590
+ function reconcile_by_key(anchor, block, b) {
500
591
  var b_length = b.length;
501
592
  var state = /** @type {ListState} */ (block.s);
593
+ var get_key = state.k;
502
594
 
503
595
  if (state.keys === null && b_length > 0) {
504
596
  var b_blocks = Array(b_length);
@@ -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, b_keys[j], state);
516
608
  }
517
609
 
518
610
  state.array = b;
@@ -521,46 +613,24 @@ function reconcile_by_key(
521
613
  return;
522
614
  }
523
615
 
524
- reconcile_by_key_diff(
525
- anchor,
526
- block,
527
- b,
528
- render_fn,
529
- is_controlled,
530
- is_indexed,
531
- get_key,
532
- render_empty,
533
- map_item,
534
- );
616
+ reconcile_by_key_diff(anchor, block, b);
535
617
  }
536
618
 
537
619
  /**
538
620
  * Keyed diff for every run after the first (or a first run with an empty
539
621
  * list). See {@link reconcile_by_key}.
540
- * @template V, K
622
+ * @template V
541
623
  * @param {ListAnchor} anchor
542
624
  * @param {Block} block
543
625
  * @param {V[]} b
544
- * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
545
- * @param {boolean} is_controlled
546
- * @param {boolean} is_indexed
547
- * @param {((item: V) => K) | undefined} get_key
548
- * @param {((anchor: Node) => void) | undefined} render_empty
549
- * @param {((item: V) => any) | undefined} map_item
550
626
  * @returns {void}
551
627
  */
552
- function reconcile_by_key_diff(
553
- anchor,
554
- block,
555
- b,
556
- render_fn,
557
- is_controlled,
558
- is_indexed,
559
- get_key,
560
- render_empty,
561
- map_item,
562
- ) {
628
+ function reconcile_by_key_diff(anchor, block, b) {
563
629
  var state = /** @type {ListState} */ (block.s);
630
+ var is_controlled = state.c;
631
+ var is_indexed = state.x;
632
+ var get_key = state.k;
633
+ var render_empty = state.e;
564
634
 
565
635
  // Variables used in conditional branches - declare with initial values
566
636
  /** @type {number} */
@@ -627,7 +697,7 @@ function reconcile_by_key_diff(
627
697
  // Fast-path for create
628
698
  if (a_length === 0) {
629
699
  for (; j < b_length; j++) {
630
- b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, true, map_item);
700
+ b_blocks[j] = create_item(anchor, b[j], j, b_keys[j], state);
631
701
  }
632
702
  state.array = b;
633
703
  state.blocks = b_blocks;
@@ -669,27 +739,15 @@ function reconcile_by_key_diff(
669
739
  // moves; only what is left afterwards needs the map and LIS below.
670
740
  while (a_start <= a_end && b_start <= b_end) {
671
741
  if (a_keys[a_start] === b_keys[b_start]) {
672
- b_val = b[b_start];
673
742
  b_block = b_blocks[b_start] = a_blocks[a_start];
674
- if (is_indexed) {
675
- update_index(b_block, b_start);
676
- }
677
- if (get_key !== undefined) {
678
- update_value(b_block, b_val, map_item);
679
- }
743
+ patch_item(state, b_block, b[b_start], b_start);
680
744
  a_start++;
681
745
  b_start++;
682
746
  continue;
683
747
  }
684
748
  if (a_keys[a_end] === b_keys[b_end]) {
685
- b_val = b[b_end];
686
749
  b_block = b_blocks[b_end] = a_blocks[a_end];
687
- if (is_indexed) {
688
- update_index(b_block, b_end);
689
- }
690
- if (get_key !== undefined) {
691
- update_value(b_block, b_val, map_item);
692
- }
750
+ patch_item(state, b_block, b[b_end], b_end);
693
751
  a_end--;
694
752
  b_end--;
695
753
  continue;
@@ -704,14 +762,8 @@ function reconcile_by_key_diff(
704
762
  }
705
763
  if (a_keys[a_end] === b_keys[b_start]) {
706
764
  // Last old item is the next new one: move it in front of the old run.
707
- b_val = b[b_start];
708
765
  b_block = b_blocks[b_start] = a_blocks[a_end];
709
- if (is_indexed) {
710
- update_index(b_block, b_start);
711
- }
712
- if (get_key !== undefined) {
713
- update_value(b_block, b_val, map_item);
714
- }
766
+ patch_item(state, b_block, b[b_start], b_start);
715
767
  move(b_block, /** @type {ChildNode} */ (a_blocks[a_start].s.start));
716
768
  a_end--;
717
769
  b_start++;
@@ -719,14 +771,8 @@ function reconcile_by_key_diff(
719
771
  }
720
772
  if (a_keys[a_start] === b_keys[b_end]) {
721
773
  // First old item is the last new one: move it behind the old run.
722
- b_val = b[b_end];
723
774
  b_block = b_blocks[b_end] = a_blocks[a_start];
724
- if (is_indexed) {
725
- update_index(b_block, b_end);
726
- }
727
- if (get_key !== undefined) {
728
- update_value(b_block, b_val, map_item);
729
- }
775
+ patch_item(state, b_block, b[b_end], b_end);
730
776
  move(b_block, block_start(b_blocks, b_end + 1, b_length, anchor));
731
777
  a_start++;
732
778
  b_end--;
@@ -741,15 +787,7 @@ function reconcile_by_key_diff(
741
787
  if (b_start <= b_end) {
742
788
  var target_node = block_start(b_blocks, b_end + 1, b_length, anchor);
743
789
  while (b_start <= b_end) {
744
- b_blocks[b_start] = create_item(
745
- target_node,
746
- b[b_start],
747
- b_start,
748
- render_fn,
749
- is_indexed,
750
- true,
751
- map_item,
752
- );
790
+ b_blocks[b_start] = create_item(target_node, b[b_start], b_start, b_keys[b_start], state);
753
791
  b_start++;
754
792
  }
755
793
  }
@@ -790,14 +828,8 @@ function reconcile_by_key_diff(
790
828
  } else {
791
829
  pos = j;
792
830
  }
793
- b_val = b[j];
794
831
  b_block = b_blocks[j] = a_blocks[i];
795
- if (is_indexed) {
796
- update_index(b_block, j);
797
- }
798
- if (get_key !== undefined) {
799
- update_value(b_block, b_val, map_item);
800
- }
832
+ patch_item(state, b_block, b[j], j);
801
833
  ++patched;
802
834
  break;
803
835
  }
@@ -833,14 +865,8 @@ function reconcile_by_key_diff(
833
865
  } else {
834
866
  pos = j;
835
867
  }
836
- b_val = b[j];
837
868
  b_block = b_blocks[j] = a_blocks[i];
838
- if (is_indexed) {
839
- update_index(b_block, j);
840
- }
841
- if (get_key !== undefined) {
842
- update_value(b_block, b_val, map_item);
843
- }
869
+ patch_item(state, b_block, b[j], j);
844
870
  ++patched;
845
871
  } else if (!fast_path_removal) {
846
872
  destroy_block(a_blocks[i]);
@@ -854,17 +880,7 @@ function reconcile_by_key_diff(
854
880
 
855
881
  if (fast_path_removal) {
856
882
  reconcile_fast_clear(anchor, block, []);
857
- reconcile_by_key(
858
- anchor,
859
- block,
860
- b,
861
- render_fn,
862
- is_controlled,
863
- is_indexed,
864
- get_key,
865
- render_empty,
866
- map_item,
867
- );
883
+ reconcile_by_key(anchor, block, b);
868
884
  return;
869
885
  } else if (moved) {
870
886
  var next_pos = 0;
@@ -872,26 +888,9 @@ function reconcile_by_key_diff(
872
888
  j = seq.length - 1;
873
889
 
874
890
  // 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.
891
+ // in order (see `relay`).
877
892
  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
- }
893
+ relay(anchor, a_blocks, a_start, a_end, b, b_blocks, b_start, b_end, sources, b_keys, state);
895
894
  state.array = b;
896
895
  state.blocks = b_blocks;
897
896
  state.keys = b_keys;
@@ -905,7 +904,7 @@ function reconcile_by_key_diff(
905
904
  next_pos = pos + 1;
906
905
 
907
906
  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);
907
+ b_blocks[pos] = create_item(target, b_val, pos, b_keys[pos], state);
909
908
  } else if (j < 0 || i !== seq[j]) {
910
909
  pos = i + b_start;
911
910
  next_pos = pos + 1;
@@ -924,7 +923,7 @@ function reconcile_by_key_diff(
924
923
  next_pos = pos + 1;
925
924
 
926
925
  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);
926
+ b_blocks[pos] = create_item(target, b_val, pos, b_keys[pos], state);
928
927
  }
929
928
  }
930
929
  }
@@ -934,19 +933,94 @@ function reconcile_by_key_diff(
934
933
  state.keys = b_keys;
935
934
  }
936
935
 
936
+ /**
937
+ * Re-lays the unmatched range `b_start..b_end` in its new order, walking it
938
+ * front to back: `cursor` is the first node of the range that is not in place
939
+ * yet, so a block already there costs a compare and a sibling read, and every
940
+ * other block (or a new item) is inserted before `cursor`. Inserting each item
941
+ * toward the front is what the reverse fast path does too; appending every
942
+ * item at the end of the range instead cost about twice as much in Blink's
943
+ * reorder floor (500 rows: 0.55 ms against 0.29), and several times more
944
+ * right after a layout flush, so this holds even when most items move.
945
+ * @template V
946
+ * @param {ListAnchor} anchor
947
+ * @param {Block[]} a_blocks the old blocks; destroyed ones are skipped
948
+ * @param {number} a_start
949
+ * @param {number} a_end
950
+ * @param {V[]} b
951
+ * @param {Block[]} b_blocks matched blocks at their new index, holes for new items
952
+ * @param {number} b_start
953
+ * @param {number} b_end
954
+ * @param {Int32Array} sources zero at the index of a new item
955
+ * @param {any[] | undefined} b_keys the new items' keys (a keyed list)
956
+ * @param {ListState} state
957
+ */
958
+ function relay(
959
+ anchor,
960
+ a_blocks,
961
+ a_start,
962
+ a_end,
963
+ b,
964
+ b_blocks,
965
+ b_start,
966
+ b_end,
967
+ sources,
968
+ b_keys,
969
+ state,
970
+ ) {
971
+ var b_length = b.length;
972
+ var end_target = block_start(b_blocks, b_end + 1, b_length, anchor);
973
+ /** @type {ChildNode | AppendIntoAnchor | null} */
974
+ var cursor = null;
975
+
976
+ // The range starts at the first surviving old block.
977
+ for (var i = a_start; i <= a_end && cursor === null; i++) {
978
+ var old_block = a_blocks[i];
979
+ if ((old_block.f & DESTROYED) === 0) {
980
+ cursor = /** @type {ChildNode | null} */ (get_first_node(old_block));
981
+ }
982
+ }
983
+ if (cursor === null) {
984
+ cursor = end_target;
985
+ }
986
+
987
+ for (var pos = b_start; pos <= b_end; pos++) {
988
+ if (sources[pos - b_start] === 0) {
989
+ b_blocks[pos] = create_item(
990
+ cursor,
991
+ b[pos],
992
+ pos,
993
+ b_keys === undefined ? undefined : b_keys[pos],
994
+ state,
995
+ );
996
+ continue;
997
+ }
998
+ var block = b_blocks[pos];
999
+ var first = get_first_node(block);
1000
+ if (first === null) {
1001
+ // Renders nothing: it has no place to take.
1002
+ continue;
1003
+ }
1004
+ if (first !== cursor) {
1005
+ move(block, cursor);
1006
+ continue;
1007
+ }
1008
+ var after = next_sibling(/** @type {Node} */ (get_last_node(block)));
1009
+ cursor = after === null ? end_target : /** @type {ChildNode} */ (after);
1010
+ }
1011
+ }
1012
+
937
1013
  /**
938
1014
  * @template V
939
1015
  * @param {ListAnchor} anchor
940
1016
  * @param {Block} block
941
1017
  * @param {V[]} b
942
- * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
943
- * @param {boolean} is_controlled
944
- * @param {boolean} is_indexed
945
- * @param {(anchor: Node) => void} [render_empty]
946
1018
  * @returns {void}
947
1019
  */
948
- function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed, render_empty) {
1020
+ function reconcile_by_ref(anchor, block, b) {
949
1021
  var state = /** @type {ListState} */ (block.s);
1022
+ var is_controlled = state.c;
1023
+ var render_empty = state.e;
950
1024
  var a_length = state.array.length;
951
1025
  var b_length = b.length;
952
1026
  var j = 0;
@@ -986,14 +1060,14 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
986
1060
  // Fast-path for create
987
1061
  if (a_length === 0) {
988
1062
  for (; j < b_length; j++) {
989
- b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, false);
1063
+ b_blocks[j] = create_item(anchor, b[j], j, undefined, state);
990
1064
  }
991
1065
  state.array = b;
992
1066
  state.blocks = b_blocks;
993
1067
  return;
994
1068
  }
995
1069
 
996
- reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_controlled, is_indexed);
1070
+ reconcile_by_ref_diff(anchor, block, b, b_blocks);
997
1071
  }
998
1072
 
999
1073
  /**
@@ -1004,13 +1078,11 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
1004
1078
  * @param {Block} block
1005
1079
  * @param {V[]} b
1006
1080
  * @param {Block[]} b_blocks
1007
- * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
1008
- * @param {boolean} is_controlled
1009
- * @param {boolean} is_indexed
1010
1081
  * @returns {void}
1011
1082
  */
1012
- function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_controlled, is_indexed) {
1083
+ function reconcile_by_ref_diff(anchor, block, b, b_blocks) {
1013
1084
  var state = /** @type {ListState} */ (block.s);
1085
+ var is_controlled = state.c;
1014
1086
 
1015
1087
  // Variables used in conditional branches - declare with initial values
1016
1088
  /** @type {number} */
@@ -1046,21 +1118,15 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1046
1118
  // moves; only what is left afterwards needs the map and LIS below.
1047
1119
  while (a_start <= a_end && b_start <= b_end) {
1048
1120
  if (a[a_start] === b[b_start]) {
1049
- b_val = b[b_start];
1050
1121
  b_block = b_blocks[b_start] = a_blocks[a_start];
1051
- if (is_indexed) {
1052
- update_index(b_block, b_start);
1053
- }
1122
+ patch_item(state, b_block, b[b_start], b_start);
1054
1123
  a_start++;
1055
1124
  b_start++;
1056
1125
  continue;
1057
1126
  }
1058
1127
  if (a[a_end] === b[b_end]) {
1059
- b_val = b[b_end];
1060
1128
  b_block = b_blocks[b_end] = a_blocks[a_end];
1061
- if (is_indexed) {
1062
- update_index(b_block, b_end);
1063
- }
1129
+ patch_item(state, b_block, b[b_end], b_end);
1064
1130
  a_end--;
1065
1131
  b_end--;
1066
1132
  continue;
@@ -1070,11 +1136,8 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1070
1136
  }
1071
1137
  if (a[a_end] === b[b_start]) {
1072
1138
  // Last old item is the next new one: move it in front of the old run.
1073
- b_val = b[b_start];
1074
1139
  b_block = b_blocks[b_start] = a_blocks[a_end];
1075
- if (is_indexed) {
1076
- update_index(b_block, b_start);
1077
- }
1140
+ patch_item(state, b_block, b[b_start], b_start);
1078
1141
  move(b_block, /** @type {ChildNode} */ (a_blocks[a_start].s.start));
1079
1142
  a_end--;
1080
1143
  b_start++;
@@ -1082,11 +1145,8 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1082
1145
  }
1083
1146
  if (a[a_start] === b[b_end]) {
1084
1147
  // First old item is the last new one: move it behind the old run.
1085
- b_val = b[b_end];
1086
1148
  b_block = b_blocks[b_end] = a_blocks[a_start];
1087
- if (is_indexed) {
1088
- update_index(b_block, b_end);
1089
- }
1149
+ patch_item(state, b_block, b[b_end], b_end);
1090
1150
  move(b_block, block_start(b_blocks, b_end + 1, b_length, anchor));
1091
1151
  a_start++;
1092
1152
  b_end--;
@@ -1101,14 +1161,7 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1101
1161
  if (b_start <= b_end) {
1102
1162
  var target_node = block_start(b_blocks, b_end + 1, b_length, anchor);
1103
1163
  while (b_start <= b_end) {
1104
- b_blocks[b_start] = create_item(
1105
- target_node,
1106
- b[b_start],
1107
- b_start,
1108
- render_fn,
1109
- is_indexed,
1110
- false,
1111
- );
1164
+ b_blocks[b_start] = create_item(target_node, b[b_start], b_start, undefined, state);
1112
1165
  b_start++;
1113
1166
  }
1114
1167
  }
@@ -1145,11 +1198,8 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1145
1198
  } else {
1146
1199
  pos = j;
1147
1200
  }
1148
- b_val = b[j];
1149
1201
  b_block = b_blocks[j] = a_blocks[i];
1150
- if (is_indexed) {
1151
- update_index(b_block, j);
1152
- }
1202
+ patch_item(state, b_block, b[j], j);
1153
1203
  ++patched;
1154
1204
  break;
1155
1205
  }
@@ -1185,11 +1235,8 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1185
1235
  } else {
1186
1236
  pos = j;
1187
1237
  }
1188
- b_val = b[j];
1189
1238
  b_block = b_blocks[j] = a_blocks[i];
1190
- if (is_indexed) {
1191
- update_index(b_block, j);
1192
- }
1239
+ patch_item(state, b_block, b[j], j);
1193
1240
  ++patched;
1194
1241
  } else if (!fast_path_removal) {
1195
1242
  destroy_block(a_blocks[i]);
@@ -1203,7 +1250,7 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1203
1250
 
1204
1251
  if (fast_path_removal) {
1205
1252
  reconcile_fast_clear(anchor, block, []);
1206
- reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed);
1253
+ reconcile_by_ref(anchor, block, b);
1207
1254
  return;
1208
1255
  } else if (moved) {
1209
1256
  var next_pos = 0;
@@ -1211,18 +1258,21 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1211
1258
  j = seq.length - 1;
1212
1259
 
1213
1260
  // 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.
1261
+ // in order (see `relay`).
1216
1262
  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
- }
1263
+ relay(
1264
+ anchor,
1265
+ a_blocks,
1266
+ a_start,
1267
+ a_end,
1268
+ b,
1269
+ b_blocks,
1270
+ b_start,
1271
+ b_end,
1272
+ sources,
1273
+ undefined,
1274
+ state,
1275
+ );
1226
1276
  state.array = b;
1227
1277
  state.blocks = b_blocks;
1228
1278
  return;
@@ -1235,7 +1285,7 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1235
1285
  next_pos = pos + 1;
1236
1286
 
1237
1287
  var target = block_start(b_blocks, next_pos, b_length, anchor);
1238
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
1288
+ b_blocks[pos] = create_item(target, b_val, pos, undefined, state);
1239
1289
  } else if (j < 0 || i !== seq[j]) {
1240
1290
  pos = i + b_start;
1241
1291
  next_pos = pos + 1;
@@ -1254,7 +1304,7 @@ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_control
1254
1304
  next_pos = pos + 1;
1255
1305
 
1256
1306
  var target = block_start(b_blocks, next_pos, b_length, anchor);
1257
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
1307
+ b_blocks[pos] = create_item(target, b_val, pos, undefined, state);
1258
1308
  }
1259
1309
  }
1260
1310
  }