ripple 0.3.127 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/package.json +10 -4
  3. package/src/runtime/element.js +23 -6
  4. package/src/runtime/index-client.js +81 -41
  5. package/src/runtime/index-server.js +1 -0
  6. package/src/runtime/internal/client/blocks.js +192 -114
  7. package/src/runtime/internal/client/component.js +4 -30
  8. package/src/runtime/internal/client/composite.js +12 -13
  9. package/src/runtime/internal/client/constants.js +14 -5
  10. package/src/runtime/internal/client/context.js +12 -19
  11. package/src/runtime/internal/client/css.js +7 -9
  12. package/src/runtime/internal/client/events.js +178 -90
  13. package/src/runtime/internal/client/expression.js +185 -124
  14. package/src/runtime/internal/client/for.js +616 -264
  15. package/src/runtime/internal/client/head.js +10 -5
  16. package/src/runtime/internal/client/html.js +1 -0
  17. package/src/runtime/internal/client/hydration.js +152 -10
  18. package/src/runtime/internal/client/if.js +239 -50
  19. package/src/runtime/internal/client/index.js +9 -13
  20. package/src/runtime/internal/client/operations.js +44 -51
  21. package/src/runtime/internal/client/portal.js +163 -85
  22. package/src/runtime/internal/client/render.js +44 -25
  23. package/src/runtime/internal/client/rpc.js +3 -3
  24. package/src/runtime/internal/client/runtime.js +799 -576
  25. package/src/runtime/internal/client/selector.js +77 -0
  26. package/src/runtime/internal/client/template.js +133 -158
  27. package/src/runtime/internal/client/transport.js +38 -0
  28. package/src/runtime/internal/client/try.js +465 -426
  29. package/src/runtime/internal/client/types.d.ts +65 -21
  30. package/src/runtime/internal/client/utils.js +1 -3
  31. package/src/runtime/internal/server/dynamic.js +20 -21
  32. package/src/runtime/internal/server/index.js +327 -275
  33. package/src/runtime/internal/server/rpc.js +3 -2
  34. package/src/runtime/internal/server/transport.js +16 -0
  35. package/src/runtime/transport.js +21 -0
  36. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -5
  37. package/tests/client/array/array.copy-within.test.tsrx +12 -12
  38. package/tests/client/array/array.derived.test.tsrx +46 -46
  39. package/tests/client/array/array.iteration.test.tsrx +10 -10
  40. package/tests/client/array/array.mutations.test.tsrx +20 -20
  41. package/tests/client/array/array.static.test.tsrx +11 -11
  42. package/tests/client/array/array.to-methods.test.tsrx +6 -6
  43. package/tests/client/async-suspend.test.tsrx +74 -75
  44. package/tests/client/basic/basic.attributes.test.tsrx +86 -83
  45. package/tests/client/basic/basic.collections.test.tsrx +12 -12
  46. package/tests/client/basic/basic.components.test.tsrx +99 -25
  47. package/tests/client/basic/basic.errors.test.tsrx +18 -18
  48. package/tests/client/basic/basic.events.test.tsrx +42 -42
  49. package/tests/client/basic/basic.get-set.test.tsrx +8 -8
  50. package/tests/client/basic/basic.reactivity.test.tsrx +97 -29
  51. package/tests/client/basic/basic.rendering.test.tsrx +25 -25
  52. package/tests/client/basic/basic.utilities.test.tsrx +3 -3
  53. package/tests/client/boundaries.test.tsrx +8 -8
  54. package/tests/client/boxed-locals.test.tsrx +452 -0
  55. package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +6 -6
  56. package/tests/client/compiler/compiler.assignments.test.tsrx +9 -73
  57. package/tests/client/compiler/compiler.attributes.test.tsrx +23 -23
  58. package/tests/client/compiler/compiler.basic.test.tsrx +15 -60
  59. package/tests/client/compiler/compiler.tracked-access.test.tsrx +18 -43
  60. package/tests/client/compiler/compiler.try-in-function.test.tsrx +2 -2
  61. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -1
  62. package/tests/client/composite/composite.dynamic-components.test.tsrx +77 -6
  63. package/tests/client/composite/composite.props.test.tsrx +149 -12
  64. package/tests/client/composite/composite.reactivity.test.tsrx +51 -52
  65. package/tests/client/composite/composite.render.test.tsrx +26 -9
  66. package/tests/client/css/scoped-styles.test.tsrx +8 -8
  67. package/tests/client/date.test.tsrx +42 -42
  68. package/tests/client/derived-release.test.tsrx +370 -0
  69. package/tests/client/dynamic-elements.test.tsrx +45 -45
  70. package/tests/client/events.test.tsrx +97 -71
  71. package/tests/client/for-selector.test.tsrx +221 -0
  72. package/tests/client/for-single-root-items.test.tsrx +118 -3
  73. package/tests/client/for.test.tsrx +332 -31
  74. package/tests/client/head.test.tsrx +19 -19
  75. package/tests/client/html.test.tsrx +11 -9
  76. package/tests/client/if-block.test.tsrx +80 -0
  77. package/tests/client/map.test.tsrx +16 -16
  78. package/tests/client/media-query.test.tsrx +7 -7
  79. package/tests/client/portal.test.tsrx +224 -26
  80. package/tests/client/primitive-text.test.tsrx +50 -0
  81. package/tests/client/prop-kind-switch.test.tsrx +141 -0
  82. package/tests/client/ref.test.tsrx +17 -17
  83. package/tests/client/return.test.tsrx +26 -3
  84. package/tests/client/root-boundary.test.tsrx +42 -0
  85. package/tests/client/scoped-flush.test.tsrx +8 -8
  86. package/tests/client/set.test.tsrx +6 -6
  87. package/tests/client/svg.test.tsrx +11 -11
  88. package/tests/client/switch.test.tsrx +44 -44
  89. package/tests/client/track-async-hydration.test.tsrx +4 -4
  90. package/tests/client/try.test.tsrx +151 -83
  91. package/tests/client/tsx.test.tsrx +43 -40
  92. package/tests/client/url/url.derived.test.tsrx +6 -6
  93. package/tests/client/url-search-params/url-search-params.derived.test.tsrx +8 -8
  94. package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +10 -10
  95. package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +10 -10
  96. package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +18 -18
  97. package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +2 -2
  98. package/tests/hydration/basic.test.js +74 -1
  99. package/tests/hydration/build-components.js +22 -1
  100. package/tests/hydration/compiled/client/basic.js +1142 -713
  101. package/tests/hydration/compiled/client/composite.js +151 -100
  102. package/tests/hydration/compiled/client/events.js +190 -150
  103. package/tests/hydration/compiled/client/for.js +1472 -1126
  104. package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
  105. package/tests/hydration/compiled/client/head.js +255 -199
  106. package/tests/hydration/compiled/client/hmr.js +54 -41
  107. package/tests/hydration/compiled/client/html-in-template.js +54 -39
  108. package/tests/hydration/compiled/client/html.js +1633 -1263
  109. package/tests/hydration/compiled/client/if-children.js +420 -270
  110. package/tests/hydration/compiled/client/if-fragment-controlflow.js +363 -317
  111. package/tests/hydration/compiled/client/if.js +229 -219
  112. package/tests/hydration/compiled/client/mixed-control-flow.js +420 -400
  113. package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1255
  114. package/tests/hydration/compiled/client/portal.js +87 -95
  115. package/tests/hydration/compiled/client/reactivity.js +140 -83
  116. package/tests/hydration/compiled/client/return.js +28 -19
  117. package/tests/hydration/compiled/client/streaming.js +390 -306
  118. package/tests/hydration/compiled/client/switch.js +258 -266
  119. package/tests/hydration/compiled/client/track-async-serialization.js +448 -280
  120. package/tests/hydration/compiled/client/try.js +173 -129
  121. package/tests/hydration/compiled/client/typed-text.js +71 -0
  122. package/tests/hydration/compiled/fixtures/money.js +10 -0
  123. package/tests/hydration/compiled/server/basic.js +123 -6
  124. package/tests/hydration/compiled/server/composite.js +16 -30
  125. package/tests/hydration/compiled/server/events.js +19 -19
  126. package/tests/hydration/compiled/server/for.js +109 -49
  127. package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
  128. package/tests/hydration/compiled/server/head.js +20 -28
  129. package/tests/hydration/compiled/server/hmr.js +2 -2
  130. package/tests/hydration/compiled/server/html.js +71 -247
  131. package/tests/hydration/compiled/server/if-children.js +107 -15
  132. package/tests/hydration/compiled/server/if.js +11 -15
  133. package/tests/hydration/compiled/server/mixed-control-flow.js +8 -8
  134. package/tests/hydration/compiled/server/portal.js +14 -22
  135. package/tests/hydration/compiled/server/reactivity.js +14 -46
  136. package/tests/hydration/compiled/server/streaming.js +19 -19
  137. package/tests/hydration/compiled/server/switch.js +8 -8
  138. package/tests/hydration/compiled/server/track-async-serialization.js +150 -23
  139. package/tests/hydration/compiled/server/try.js +8 -8
  140. package/tests/hydration/compiled/server/typed-text.js +17 -0
  141. package/tests/hydration/components/basic.tsrx +44 -8
  142. package/tests/hydration/components/composite.tsrx +2 -2
  143. package/tests/hydration/components/events.tsrx +25 -25
  144. package/tests/hydration/components/for.tsrx +115 -67
  145. package/tests/hydration/components/fragment-cursor.tsrx +650 -0
  146. package/tests/hydration/components/head.tsrx +16 -16
  147. package/tests/hydration/components/hmr.tsrx +2 -2
  148. package/tests/hydration/components/html.tsrx +7 -7
  149. package/tests/hydration/components/if-children.tsrx +72 -25
  150. package/tests/hydration/components/if.tsrx +18 -18
  151. package/tests/hydration/components/mixed-control-flow.tsrx +11 -11
  152. package/tests/hydration/components/portal.tsrx +3 -3
  153. package/tests/hydration/components/reactivity.tsrx +16 -16
  154. package/tests/hydration/components/streaming.tsrx +20 -20
  155. package/tests/hydration/components/switch.tsrx +20 -20
  156. package/tests/hydration/components/text-types.ts +4 -0
  157. package/tests/hydration/components/track-async-serialization.tsrx +80 -26
  158. package/tests/hydration/components/try.tsrx +8 -8
  159. package/tests/hydration/components/typed-text.tsrx +22 -0
  160. package/tests/hydration/fixtures/money.js +10 -0
  161. package/tests/hydration/for.test.js +36 -0
  162. package/tests/hydration/fragment-cursor.test.js +111 -0
  163. package/tests/hydration/if-children.test.js +24 -0
  164. package/tests/hydration/streaming.test.js +19 -0
  165. package/tests/hydration/track-async-serialization.test.js +87 -2
  166. package/tests/hydration/tsconfig.text-types.json +10 -0
  167. package/tests/hydration/typed-text.test.js +34 -0
  168. package/tests/server/await.test.tsrx +3 -3
  169. package/tests/server/basic.attributes.test.tsrx +42 -42
  170. package/tests/server/basic.components.test.tsrx +64 -8
  171. package/tests/server/basic.test.tsrx +34 -32
  172. package/tests/server/compiler.test.tsrx +1 -70
  173. package/tests/server/composite.props.test.tsrx +6 -6
  174. package/tests/server/dynamic-elements.test.tsrx +31 -8
  175. package/tests/server/for.test.tsrx +94 -0
  176. package/tests/server/head.test.tsrx +11 -11
  177. package/tests/server/scoped-styles.test.tsrx +2 -2
  178. package/tests/server/streaming-ssr.test.tsrx +48 -43
  179. package/tests/server/track-async-serialization.test.tsrx +204 -21
  180. package/tests/server/try.test.tsrx +48 -48
  181. package/tests/types/transport.ts +40 -0
  182. package/tests/utils/vite-plugin-config.test.js +23 -0
  183. package/types/index.d.ts +48 -7
  184. package/types/server.d.ts +3 -0
  185. package/types/transport.d.ts +15 -0
  186. package/src/runtime/internal/client/switch.js +0 -113
  187. package/src/utils/errors.js +0 -13
  188. package/tests/client/lazy-array.test.tsrx +0 -31
  189. package/tests/client/lazy-destructuring.test.tsrx +0 -438
  190. package/tests/client/tracked-index-access.test.tsrx +0 -113
  191. package/tests/server/lazy-destructuring.test.tsrx +0 -595
  192. package/tests/server/tracked-index-access.test.tsrx +0 -76
@@ -1,84 +1,110 @@
1
- /** @import { Block, Tracked } from '#client' */
1
+ /** @import { AppendIntoAnchor, Block, Tracked } from '#client' */
2
2
 
3
3
  import { IS_CONTROLLED, IS_INDEXED, ROOT_CONTROLLED } from '../../../constants.js';
4
4
  import {
5
+ block as create_block,
5
6
  branch,
6
7
  destroy_block,
7
8
  destroy_block_children,
8
9
  get_first_node,
9
10
  get_last_node,
11
+ own_anchor,
10
12
  render,
11
13
  } from './blocks.js';
12
- import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
13
- import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
14
- import { create_text, get_first_child, get_last_child, next_sibling } from './operations.js';
14
+ import { BRANCH_BLOCK, FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
15
+ import { hydrate_first_child, hydrate_next, hydrate_node, hydrating } from './hydration.js';
16
+ import { get_last_child, next_sibling, resolve_anchor } from './operations.js';
15
17
  import { append } from './template.js';
16
- import { active_block, set, tracked, untrack } from './runtime.js';
18
+ import { active_block, set, set_tracking, tracked } from './runtime.js';
17
19
  import { array_from, is_array } from '@tsrx/core/runtime/language-helpers';
18
20
 
19
21
  /**
20
22
  * @template V
21
- * @param {Node} anchor
23
+ * @param {Node | AppendIntoAnchor} anchor
22
24
  * @param {V} value
23
25
  * @param {number} index
24
26
  * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
25
27
  * @param {boolean} is_indexed
26
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`)
27
30
  * @returns {Block}
28
31
  */
29
- function create_item(anchor, value, index, render_fn, is_indexed, is_keyed) {
30
- var b = branch(() => {
31
- var tracked_index;
32
- /** @type {V | Tracked} */
33
- var tracked_value = value;
34
-
35
- if (is_indexed || is_keyed) {
36
- var block = /** @type {Block} */ (active_block);
37
-
38
- if (block.s === null) {
39
- if (is_indexed) {
40
- tracked_index = tracked(index, block);
41
- }
42
- if (is_keyed) {
43
- tracked_value = tracked(value, block);
44
- }
45
-
46
- block.s = {
47
- start: null,
48
- end: null,
49
- i: tracked_index,
50
- v: tracked_value,
51
- };
52
- } else {
53
- if (is_indexed) {
54
- tracked_index = block.s.i;
55
- }
56
- if (is_keyed) {
57
- tracked_index = block.s.v;
58
- }
59
- }
60
- render_fn(anchor, tracked_value, tracked_index);
61
- } else {
62
- render_fn(anchor, tracked_value);
63
- }
64
- });
32
+ function create_item(anchor, value, index, render_fn, is_indexed, is_keyed, map_item) {
33
+ var block = /** @type {Block} */ (active_block);
34
+ var tracked_index = is_indexed ? tracked(index, block) : undefined;
35
+ var tracked_value = is_keyed
36
+ ? tracked(map_item === undefined ? value : map_item(value), block)
37
+ : value;
38
+ var state = {
39
+ start: null,
40
+ end: null,
41
+ i: tracked_index,
42
+ v: tracked_value,
43
+ // The item as the collection holds it; a keyed diff compares against it.
44
+ r: value,
45
+ };
46
+
47
+ // Passed through module state rather than a per-item closure; run_item
48
+ // reads them before rendering, so nested loops cannot observe a stale pair.
49
+ item_anchor = anchor;
50
+ item_render_fn = render_fn;
51
+ var b = create_block(BRANCH_BLOCK, run_item, state);
52
+
53
+ // The item's tracked value and index are owned by the item block itself.
54
+ if (is_keyed) {
55
+ /** @type {Tracked} */ (tracked_value).b = b;
56
+ }
57
+ if (is_indexed) {
58
+ /** @type {Tracked} */ (tracked_index).b = b;
59
+ }
65
60
  return b;
66
61
  }
67
62
 
68
63
  /**
69
- * @param {Node} anchor
64
+ * The insertion point of a list: a node the items go before, or, for a
65
+ * controlled list (the sole content of its parent), an append-into sentinel so
66
+ * items append to the parent with no anchor node in the DOM.
67
+ * @typedef {Element | Text | AppendIntoAnchor} ListAnchor
68
+ */
69
+
70
+ /**
71
+ * @param {Element} parent
72
+ * @returns {AppendIntoAnchor}
73
+ */
74
+ function controlled_anchor(parent) {
75
+ return { parent, into: true };
76
+ }
77
+
78
+ /** @type {Node | AppendIntoAnchor | null} */
79
+ var item_anchor = null;
80
+ /** @type {((anchor: Node, value: any, index?: any) => Block) | null} */
81
+ var item_render_fn = null;
82
+
83
+ /**
84
+ * @param {{ i: Tracked | undefined, v: any }} state
85
+ */
86
+ function run_item(state) {
87
+ var render_fn = /** @type {(anchor: Node, value: any, index?: any) => Block} */ (item_render_fn);
88
+ var anchor = /** @type {Node} */ (item_anchor);
89
+ item_render_fn = null;
90
+ item_anchor = null;
91
+ render_fn(anchor, state.v, state.i);
92
+ }
93
+
94
+ /**
95
+ * @param {ListAnchor} anchor
70
96
  * @param {(anchor: Node) => void} render_empty
71
97
  * @returns {Block}
72
98
  */
73
99
  function create_empty(anchor, render_empty) {
74
100
  return branch(() => {
75
- render_empty(anchor);
101
+ render_empty(/** @type {Node} */ (anchor));
76
102
  });
77
103
  }
78
104
 
79
105
  /**
80
106
  * @param {Block} block
81
- * @param {ChildNode} anchor
107
+ * @param {ChildNode | AppendIntoAnchor} anchor
82
108
  * @returns {void}
83
109
  */
84
110
  function move(block, anchor) {
@@ -99,21 +125,46 @@ function move(block, anchor) {
99
125
  end = s.end;
100
126
  }
101
127
 
128
+ // A controlled list appends into its parent (no anchor node); anything
129
+ // else inserts before its anchor.
130
+ /** @type {Node | null} */
131
+ var parent = null;
132
+ /** @type {ChildNode | null} */
133
+ var before = null;
134
+ if (/** @type {AppendIntoAnchor} */ (anchor).into === true) {
135
+ parent = /** @type {AppendIntoAnchor} */ (anchor).parent;
136
+ } else {
137
+ before = /** @type {ChildNode} */ (anchor);
138
+ }
139
+
102
140
  if (node === end) {
103
- anchor.before(node);
141
+ insert_node(parent, before, node);
104
142
  return;
105
143
  }
106
144
  while (node !== null) {
107
145
  var next_node = /** @type {Node} */ (next_sibling(node));
108
- anchor.before(node);
146
+ insert_node(parent, before, node);
109
147
  node = next_node;
110
148
  if (node === end) {
111
- anchor.before(/** @type {Node} */ (end));
149
+ insert_node(parent, before, /** @type {Node} */ (end));
112
150
  break;
113
151
  }
114
152
  }
115
153
  }
116
154
 
155
+ /**
156
+ * @param {Node | null} parent
157
+ * @param {ChildNode | null} before
158
+ * @param {Node} node
159
+ */
160
+ function insert_node(parent, before, node) {
161
+ if (parent !== null) {
162
+ parent.appendChild(node);
163
+ } else {
164
+ /** @type {ChildNode} */ (before).before(node);
165
+ }
166
+ }
167
+
117
168
  /**
118
169
  * Resolve the insertion anchor for a block at `index`: the first real DOM node
119
170
  * at or after `index`, or `fallback` when every remaining block renders nothing.
@@ -123,8 +174,8 @@ function move(block, anchor) {
123
174
  * @param {Block[]} blocks
124
175
  * @param {number} index
125
176
  * @param {number} length
126
- * @param {Element | Text} fallback
127
- * @returns {ChildNode}
177
+ * @param {ListAnchor} fallback
178
+ * @returns {ChildNode | AppendIntoAnchor}
128
179
  */
129
180
  function block_start(blocks, index, length, fallback) {
130
181
  if (index >= length) {
@@ -162,9 +213,91 @@ function collection_to_array(collection) {
162
213
  return array;
163
214
  }
164
215
 
216
+ /**
217
+ * The state of a list block. The reconciled list (`array`, `blocks`, `keys`,
218
+ * `empty`) and the inputs the list is re-run with both live here, so a list
219
+ * allocates no closures.
220
+ */
221
+ class ListState {
222
+ /** @type {any[]} */
223
+ array = [];
224
+ /** @type {Block[]} */
225
+ blocks = [];
226
+ /** @type {any[] | null} null until the first reconcile */
227
+ keys = null;
228
+ /** @type {Block | null} */
229
+ empty = null;
230
+
231
+ /**
232
+ * @param {ListAnchor} anchor
233
+ * @param {() => any} get_collection
234
+ * @param {(anchor: Node, value: any, index?: any) => Block} render_fn
235
+ * @param {boolean} is_controlled
236
+ * @param {boolean} is_indexed
237
+ * @param {((item: any) => any) | undefined} get_key
238
+ * @param {((anchor: Node) => void) | undefined} render_empty
239
+ * @param {((item: any) => any) | undefined} [map_item]
240
+ */
241
+ constructor(
242
+ anchor,
243
+ get_collection,
244
+ render_fn,
245
+ is_controlled,
246
+ is_indexed,
247
+ get_key,
248
+ render_empty,
249
+ map_item,
250
+ ) {
251
+ this.a = anchor;
252
+ this.g = get_collection;
253
+ this.r = render_fn;
254
+ this.c = is_controlled;
255
+ this.x = is_indexed;
256
+ this.k = get_key;
257
+ this.e = render_empty;
258
+ this.m = map_item;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * @param {ListState} state
264
+ */
265
+ function run_for(state) {
266
+ var block = /** @type {Block} */ (active_block);
267
+ var array = collection_to_array(state.g());
268
+
269
+ // Items render untracked, as in a branch; the list tracks only its collection.
270
+ set_tracking(false);
271
+ reconcile_by_ref(state.a, block, array, state.r, state.c, state.x, state.e);
272
+ set_tracking(true);
273
+
274
+ // Re-anchor a hydrated list: the hydrated anchor is the block's start
275
+ // marker; later inserts and end moves must go before the cursor, which now
276
+ // sits after the hydrated items.
277
+ if (hydrating) {
278
+ state.a = /** @type {Element | Text} */ (hydrate_node);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * @param {ListState} state
284
+ */
285
+ function run_for_keyed(state) {
286
+ var block = /** @type {Block} */ (active_block);
287
+ var array = collection_to_array(state.g());
288
+
289
+ set_tracking(false);
290
+ reconcile_by_key(state.a, block, array, state.r, state.c, state.x, state.k, state.e, state.m);
291
+ set_tracking(true);
292
+
293
+ if (hydrating) {
294
+ state.a = /** @type {Element | Text} */ (hydrate_node);
295
+ }
296
+ }
297
+
165
298
  /**
166
299
  * @template V
167
- * @param {Element} node
300
+ * @param {Element | AppendIntoAnchor} node
168
301
  * @param {() => V[] | Iterable<V>} get_collection
169
302
  * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
170
303
  * @param {number} flags
@@ -175,16 +308,18 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
175
308
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
176
309
  var is_indexed = (flags & IS_INDEXED) !== 0;
177
310
  var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
178
- var anchor = /** @type {Element | Text} */ (node);
311
+ // A root-controlled list receives the component's `__anchor`, which may be
312
+ // an append-into sentinel; moves and end insertions need a real node.
313
+ var anchor = /** @type {ListAnchor} */ (root_controlled ? resolve_anchor(node) : node);
179
314
  /** @type {Node | undefined} */
180
315
  var boundary;
181
316
 
182
317
  if (is_controlled) {
183
318
  if (hydrating) {
184
- var parent_node = /** @type {Element} */ (node);
185
- /** @type {Element | Text} */ (set_hydrate_node(get_first_child(parent_node)));
319
+ // The cursor sits on the list's element; the items are its children.
320
+ hydrate_first_child();
186
321
  } else {
187
- anchor = node.appendChild(create_text());
322
+ anchor = controlled_anchor(/** @type {Element} */ (node));
188
323
  }
189
324
  }
190
325
 
@@ -196,24 +331,24 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
196
331
  }
197
332
 
198
333
  render(
199
- () => {
200
- var block = /** @type {Block} */ (active_block);
201
- var collection = get_collection();
202
- var array = collection_to_array(collection);
203
-
204
- untrack(() => {
205
- reconcile_by_ref(anchor, block, array, render_fn, is_controlled, is_indexed, render_empty);
206
- });
207
-
208
- if (hydrating) {
209
- anchor = /** @type {Element | Text} */ (hydrate_node);
210
- }
211
- },
212
- null,
334
+ run_for,
335
+ new ListState(
336
+ anchor,
337
+ get_collection,
338
+ render_fn,
339
+ is_controlled,
340
+ is_indexed,
341
+ undefined,
342
+ render_empty,
343
+ ),
213
344
  FOR_BLOCK,
214
345
  );
215
346
 
347
+ if (!is_controlled) own_anchor(node, /** @type {Node} */ (anchor));
348
+
216
349
  if (hydrating && root_controlled) {
350
+ // The original `node`: for a sentinel, `hydrate_append` performs the
351
+ // cursor advance that stands in for the eliminated sibling navigation.
217
352
  append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
218
353
  }
219
354
  }
@@ -221,19 +356,30 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
221
356
  /**
222
357
  * @template V
223
358
  * @template K
224
- * @param {Element} node
359
+ * @param {Element | AppendIntoAnchor} node
225
360
  * @param {() => V[] | Iterable<V>} get_collection
226
361
  * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
227
362
  * @param {number} flags
228
- * @param {(item: V) => K} [get_key]
363
+ * @param {(item: V) => K} [get_key] omitted for identity keys (`key item`)
229
364
  * @param {(anchor: Node) => void} [render_empty]
365
+ * @param {(item: V) => any} [map_item] for a destructuring pattern with a rest
366
+ * element or a default: destructures the item once, natively, into an object
367
+ * of the pattern's names, which is what the item's tracked then holds
230
368
  * @returns {void}
231
369
  */
232
- export function for_block_keyed(node, get_collection, render_fn, flags, get_key, render_empty) {
370
+ export function for_block_keyed(
371
+ node,
372
+ get_collection,
373
+ render_fn,
374
+ flags,
375
+ get_key,
376
+ render_empty,
377
+ map_item,
378
+ ) {
233
379
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
234
380
  var is_indexed = (flags & IS_INDEXED) !== 0;
235
381
  var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
236
- var anchor = /** @type {Element | Text} */ (node);
382
+ var anchor = /** @type {ListAnchor} */ (root_controlled ? resolve_anchor(node) : node);
237
383
  /** @type {Node | undefined} */
238
384
  var boundary;
239
385
 
@@ -241,10 +387,10 @@ export function for_block_keyed(node, get_collection, render_fn, flags, get_key,
241
387
  var parent_node = /** @type {Element} */ (node);
242
388
 
243
389
  if (hydrating) {
244
- /** @type {Element | Text} */ (set_hydrate_node(get_first_child(parent_node)));
390
+ hydrate_first_child();
245
391
  anchor = /** @type {Element | Text} */ (get_last_child(parent_node));
246
392
  } else {
247
- anchor = node.appendChild(create_text());
393
+ anchor = controlled_anchor(parent_node);
248
394
  }
249
395
  }
250
396
 
@@ -256,46 +402,45 @@ export function for_block_keyed(node, get_collection, render_fn, flags, get_key,
256
402
  }
257
403
 
258
404
  render(
259
- () => {
260
- var block = /** @type {Block} */ (active_block);
261
- var collection = get_collection();
262
- var array = collection_to_array(collection);
263
-
264
- untrack(() => {
265
- reconcile_by_key(
266
- anchor,
267
- block,
268
- array,
269
- render_fn,
270
- is_controlled,
271
- is_indexed,
272
- /** @type {(item: V) => K} */ (get_key),
273
- render_empty,
274
- );
275
- });
276
- },
277
- null,
405
+ run_for_keyed,
406
+ new ListState(
407
+ anchor,
408
+ get_collection,
409
+ render_fn,
410
+ is_controlled,
411
+ is_indexed,
412
+ get_key,
413
+ render_empty,
414
+ map_item,
415
+ ),
278
416
  FOR_BLOCK,
279
417
  );
280
418
 
419
+ if (!is_controlled) own_anchor(node, /** @type {Node} */ (anchor));
420
+
281
421
  if (hydrating && root_controlled) {
422
+ // The original `node`: for a sentinel, `hydrate_append` performs the
423
+ // cursor advance that stands in for the eliminated sibling navigation.
282
424
  append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
283
425
  }
284
426
  }
285
427
 
286
428
  /**
287
429
  * @template V
288
- * @param {Element | Text} anchor
430
+ * @param {ListAnchor} anchor
289
431
  * @param {Block} block
290
432
  * @param {V[]} array
291
433
  * @returns {void}
292
434
  */
293
435
  function reconcile_fast_clear(anchor, block, array) {
294
436
  var state = block.s;
295
- var parent_node = /** @type {Element} */ (anchor.parentNode);
437
+ var into = /** @type {AppendIntoAnchor} */ (anchor).into === true;
438
+ var parent_node = /** @type {Element} */ (
439
+ into ? /** @type {AppendIntoAnchor} */ (anchor).parent : /** @type {Node} */ (anchor).parentNode
440
+ );
296
441
  parent_node.textContent = '';
297
442
  destroy_block_children(block);
298
- parent_node.append(anchor);
443
+ if (!into) parent_node.append(/** @type {Node} */ (anchor));
299
444
  state.array = array;
300
445
  state.blocks = [];
301
446
  state.empty = null;
@@ -313,24 +458,33 @@ function update_index(block, index) {
313
458
  /**
314
459
  * @param {Block} block
315
460
  * @param {any} value
461
+ * @param {((item: any) => any) | undefined} map_item
316
462
  * @returns {void}
317
463
  */
318
- function update_value(block, value) {
319
- set(block.s.v, value);
464
+ function update_value(block, value, map_item) {
465
+ var state = block.s;
466
+ if (state.r !== value) {
467
+ state.r = value;
468
+ set(state.v, map_item === undefined ? value : map_item(value));
469
+ }
320
470
  }
321
471
 
322
472
  /**
323
473
  * @template V
324
474
  * @template K
325
- * @param {Element | Text} anchor
475
+ * @param {ListAnchor} anchor
326
476
  * @param {Block} block
327
477
  * @param {V[]} b
328
478
  * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
329
479
  * @param {boolean} is_controlled
330
480
  * @param {boolean} is_indexed
331
- * @param {(item: V) => K} get_key
332
- * @param {(anchor: Node) => void} [render_empty]
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
333
484
  * @returns {void}
485
+ *
486
+ * The first run only creates items, so it lives in this small function and
487
+ * the diff below is compiled only once a list actually changes.
334
488
  */
335
489
  function reconcile_by_key(
336
490
  anchor,
@@ -341,15 +495,75 @@ function reconcile_by_key(
341
495
  is_indexed,
342
496
  get_key,
343
497
  render_empty,
498
+ map_item,
344
499
  ) {
345
- var state = block.s;
500
+ var b_length = b.length;
501
+ var state = /** @type {ListState} */ (block.s);
502
+
503
+ if (state.keys === null && b_length > 0) {
504
+ var b_blocks = Array(b_length);
505
+ // Identity keys (`key item`) are the items themselves; the list keeps a
506
+ // copy, as the array it rendered may later be mutated in place.
507
+ var b_keys = get_key === undefined ? b.slice() : Array(b_length);
508
+
509
+ // One loop, no `map` callback machinery: most lists are short.
510
+ for (var j = 0; j < b_length; j++) {
511
+ var value = b[j];
512
+ if (get_key !== undefined) {
513
+ b_keys[j] = get_key(value);
514
+ }
515
+ b_blocks[j] = create_item(anchor, value, j, render_fn, is_indexed, true, map_item);
516
+ }
517
+
518
+ state.array = b;
519
+ state.blocks = b_blocks;
520
+ state.keys = b_keys;
521
+ return;
522
+ }
523
+
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
+ );
535
+ }
536
+
537
+ /**
538
+ * Keyed diff for every run after the first (or a first run with an empty
539
+ * list). See {@link reconcile_by_key}.
540
+ * @template V, K
541
+ * @param {ListAnchor} anchor
542
+ * @param {Block} block
543
+ * @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
+ * @returns {void}
551
+ */
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
+ ) {
563
+ var state = /** @type {ListState} */ (block.s);
346
564
 
347
565
  // Variables used in conditional branches - declare with initial values
348
566
  /** @type {number} */
349
- var a_start = 0;
350
- /** @type {number} */
351
- var b_start = 0;
352
- /** @type {number} */
353
567
  var a_left = 0;
354
568
  /** @type {number} */
355
569
  var b_left = 0;
@@ -364,17 +578,8 @@ function reconcile_by_key(
364
578
  /** @type {number} */
365
579
  var i = 0;
366
580
 
367
- if (state === null) {
368
- state = block.s = {
369
- array: [],
370
- blocks: [],
371
- keys: null,
372
- empty: null,
373
- };
374
- }
375
-
376
- var a = state.array;
377
- var a_length = a.length;
581
+ var a_blocks = state.blocks;
582
+ var a_length = a_blocks.length;
378
583
  var b_length = b.length;
379
584
  var j = 0;
380
585
 
@@ -409,13 +614,20 @@ function reconcile_by_key(
409
614
  }
410
615
  return;
411
616
  }
412
- var b_blocks = Array(b_length);
413
- var b_keys = b.map(get_key);
617
+ // Identity keys are the items themselves (copied, as the rendered array
618
+ // may later be mutated in place), so a matched key is also the item's
619
+ // current tracked value: only computed keys need `update_value`.
620
+ var b_keys = get_key === undefined ? b.slice() : b.map(get_key);
621
+ // A same-length run rewrites the block array in place: matched ends are
622
+ // already at their index, and the middle reads the old blocks from a copy
623
+ // taken before its first write.
624
+ var in_place = a_length === b_length;
625
+ var b_blocks = in_place ? a_blocks : Array(b_length);
414
626
 
415
627
  // Fast-path for create
416
628
  if (a_length === 0) {
417
629
  for (; j < b_length; j++) {
418
- b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, true);
630
+ b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, true, map_item);
419
631
  }
420
632
  state.array = b;
421
633
  state.blocks = b_blocks;
@@ -423,78 +635,135 @@ function reconcile_by_key(
423
635
  return;
424
636
  }
425
637
 
426
- var a_blocks = state.blocks;
427
- var a_keys = state.keys;
428
- var a_val = a[j];
429
- var b_val = b[j];
430
- var a_key = a_keys[j];
431
- var b_key = b_keys[j];
638
+ // Set by every run that leaves items behind.
639
+ var a_keys = /** @type {any[]} */ (state.keys);
640
+ var a_start = 0;
641
+ var b_start = 0;
432
642
  var a_end = a_length - 1;
433
643
  var b_end = b_length - 1;
644
+ var b_val;
434
645
  var b_block;
435
646
 
436
- outer: {
437
- while (a_key === b_key) {
438
- a[j] = b_val;
439
- b_block = b_blocks[j] = a_blocks[j];
440
- if (is_indexed) {
441
- update_index(b_block, j);
647
+ if (get_key === undefined && !is_indexed) {
648
+ // Identity keys without an index: a matched item needs no update, so
649
+ // the unchanged prefix and suffix are skipped with plain compares.
650
+ while (a_start <= a_end && b_start <= b_end && a_keys[a_start] === b_keys[b_start]) {
651
+ if (!in_place) {
652
+ b_blocks[b_start] = a_blocks[a_start];
442
653
  }
443
- update_value(b_block, b_val);
444
- ++j;
445
- if (j > a_end || j > b_end) {
446
- break outer;
654
+ a_start++;
655
+ b_start++;
656
+ }
657
+ while (a_start <= a_end && b_start <= b_end && a_keys[a_end] === b_keys[b_end]) {
658
+ if (!in_place) {
659
+ b_blocks[b_end] = a_blocks[a_end];
447
660
  }
448
- a_val = a[j];
449
- b_val = b[j];
450
- a_key = a_keys[j];
451
- b_key = b_keys[j];
661
+ a_end--;
662
+ b_end--;
452
663
  }
664
+ }
453
665
 
454
- a_val = a[a_end];
455
- b_val = b[b_end];
456
- a_key = a_keys[a_end];
457
- b_key = b_keys[b_end];
458
-
459
- while (a_key === b_key) {
460
- a[a_end] = b_val;
666
+ // Match from both ends first, including the two end-crossing cases: an old
667
+ // item that moved to the far end of the new list, and a run whose ends were
668
+ // exchanged (a reversal, or a swap of two items). Those complete with plain
669
+ // moves; only what is left afterwards needs the map and LIS below.
670
+ while (a_start <= a_end && b_start <= b_end) {
671
+ if (a_keys[a_start] === b_keys[b_start]) {
672
+ b_val = b[b_start];
673
+ 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
+ }
680
+ a_start++;
681
+ b_start++;
682
+ continue;
683
+ }
684
+ if (a_keys[a_end] === b_keys[b_end]) {
685
+ b_val = b[b_end];
461
686
  b_block = b_blocks[b_end] = a_blocks[a_end];
462
687
  if (is_indexed) {
463
688
  update_index(b_block, b_end);
464
689
  }
465
- update_value(b_block, b_val);
690
+ if (get_key !== undefined) {
691
+ update_value(b_block, b_val, map_item);
692
+ }
466
693
  a_end--;
467
694
  b_end--;
468
- if (j > a_end || j > b_end) {
469
- break outer;
695
+ continue;
696
+ }
697
+ if (a_start === a_end || a_blocks[a_start].s.start === null) {
698
+ break;
699
+ }
700
+ if (in_place) {
701
+ // An end-crossing move overwrites an old block before it is read.
702
+ a_blocks = a_blocks.slice();
703
+ in_place = false;
704
+ }
705
+ if (a_keys[a_end] === b_keys[b_start]) {
706
+ // Last old item is the next new one: move it in front of the old run.
707
+ b_val = b[b_start];
708
+ b_block = b_blocks[b_start] = a_blocks[a_end];
709
+ if (is_indexed) {
710
+ update_index(b_block, b_start);
470
711
  }
471
- a_val = a[a_end];
712
+ if (get_key !== undefined) {
713
+ update_value(b_block, b_val, map_item);
714
+ }
715
+ move(b_block, /** @type {ChildNode} */ (a_blocks[a_start].s.start));
716
+ a_end--;
717
+ b_start++;
718
+ continue;
719
+ }
720
+ if (a_keys[a_start] === b_keys[b_end]) {
721
+ // First old item is the last new one: move it behind the old run.
472
722
  b_val = b[b_end];
473
- a_key = a_keys[a_end];
474
- b_key = b_keys[b_end];
723
+ 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
+ }
730
+ move(b_block, block_start(b_blocks, b_end + 1, b_length, anchor));
731
+ a_start++;
732
+ b_end--;
733
+ continue;
475
734
  }
735
+ break;
476
736
  }
477
737
 
478
738
  var fast_path_removal = false;
479
739
 
480
- if (j > a_end) {
481
- if (j <= b_end) {
482
- var insert_target = block_start(a_blocks, a_end + 1, a_length, anchor);
483
- while (j <= b_end) {
484
- b_val = b[j];
485
- b_blocks[j] = create_item(insert_target, b_val, j, render_fn, is_indexed, true);
486
- j++;
740
+ if (a_start > a_end) {
741
+ if (b_start <= b_end) {
742
+ var target_node = block_start(b_blocks, b_end + 1, b_length, anchor);
743
+ 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
+ );
753
+ b_start++;
487
754
  }
488
755
  }
489
- } else if (j > b_end) {
490
- while (j <= a_end) {
491
- destroy_block(a_blocks[j++]);
756
+ } else if (b_start > b_end) {
757
+ while (a_start <= a_end) {
758
+ destroy_block(a_blocks[a_start++]);
492
759
  }
493
760
  } else {
494
- a_start = j;
495
- b_start = j;
496
- a_left = a_end - j + 1;
497
- b_left = b_end - j + 1;
761
+ if (in_place) {
762
+ a_blocks = a_blocks.slice();
763
+ in_place = false;
764
+ }
765
+ a_left = a_end - a_start + 1;
766
+ b_left = b_end - b_start + 1;
498
767
  sources = new Int32Array(b_left + 1);
499
768
  moved = false;
500
769
  pos = 0;
@@ -506,13 +775,9 @@ function reconcile_by_key(
506
775
  // When sizes are small, just loop them through
507
776
  if (b_length < 4 || (a_left | b_left) < 32) {
508
777
  for (i = a_start; i <= a_end; ++i) {
509
- a_val = a[i];
510
- a_key = a_keys[i];
511
778
  if (patched < b_left) {
512
779
  for (j = b_start; j <= b_end; j++) {
513
- b_val = b[j];
514
- b_key = b_keys[j];
515
- if (a_key === b_key) {
780
+ if (a_keys[i] === b_keys[j]) {
516
781
  sources[j - b_start] = i + 1;
517
782
  if (fast_path_removal) {
518
783
  fast_path_removal = false;
@@ -525,11 +790,14 @@ function reconcile_by_key(
525
790
  } else {
526
791
  pos = j;
527
792
  }
793
+ b_val = b[j];
528
794
  b_block = b_blocks[j] = a_blocks[i];
529
795
  if (is_indexed) {
530
796
  update_index(b_block, j);
531
797
  }
532
- update_value(b_block, b_val);
798
+ if (get_key !== undefined) {
799
+ update_value(b_block, b_val, map_item);
800
+ }
533
801
  ++patched;
534
802
  break;
535
803
  }
@@ -549,11 +817,8 @@ function reconcile_by_key(
549
817
  }
550
818
 
551
819
  for (i = a_start; i <= a_end; ++i) {
552
- a_val = a[i];
553
- a_key = a_keys[i];
554
-
555
820
  if (patched < b_left) {
556
- j = map.get(a_key);
821
+ j = map.get(a_keys[i]);
557
822
 
558
823
  if (j !== undefined) {
559
824
  if (fast_path_removal) {
@@ -568,12 +833,14 @@ function reconcile_by_key(
568
833
  } else {
569
834
  pos = j;
570
835
  }
571
- block = b_blocks[j] = a_blocks[i];
572
836
  b_val = b[j];
837
+ b_block = b_blocks[j] = a_blocks[i];
573
838
  if (is_indexed) {
574
- update_index(block, j);
839
+ update_index(b_block, j);
840
+ }
841
+ if (get_key !== undefined) {
842
+ update_value(b_block, b_val, map_item);
575
843
  }
576
- update_value(block, b_val);
577
844
  ++patched;
578
845
  } else if (!fast_path_removal) {
579
846
  destroy_block(a_blocks[i]);
@@ -587,13 +854,50 @@ function reconcile_by_key(
587
854
 
588
855
  if (fast_path_removal) {
589
856
  reconcile_fast_clear(anchor, block, []);
590
- reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed, get_key);
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
+ );
591
868
  return;
592
869
  } else if (moved) {
593
870
  var next_pos = 0;
594
871
  var seq = lis_algorithm(sources);
595
872
  j = seq.length - 1;
596
873
 
874
+ // 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.
877
+ 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
+ }
895
+ state.array = b;
896
+ state.blocks = b_blocks;
897
+ state.keys = b_keys;
898
+ return;
899
+ }
900
+
597
901
  for (i = b_left - 1; i >= 0; i--) {
598
902
  if (sources[i] === 0) {
599
903
  pos = i + b_start;
@@ -601,10 +905,9 @@ function reconcile_by_key(
601
905
  next_pos = pos + 1;
602
906
 
603
907
  var target = block_start(b_blocks, next_pos, b_length, anchor);
604
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
908
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true, map_item);
605
909
  } else if (j < 0 || i !== seq[j]) {
606
910
  pos = i + b_start;
607
- b_val = b[pos];
608
911
  next_pos = pos + 1;
609
912
 
610
913
  var target = block_start(b_blocks, next_pos, b_length, anchor);
@@ -621,7 +924,7 @@ function reconcile_by_key(
621
924
  next_pos = pos + 1;
622
925
 
623
926
  var target = block_start(b_blocks, next_pos, b_length, anchor);
624
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
927
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true, map_item);
625
928
  }
626
929
  }
627
930
  }
@@ -633,7 +936,7 @@ function reconcile_by_key(
633
936
 
634
937
  /**
635
938
  * @template V
636
- * @param {Element | Text} anchor
939
+ * @param {ListAnchor} anchor
637
940
  * @param {Block} block
638
941
  * @param {V[]} b
639
942
  * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
@@ -643,39 +946,8 @@ function reconcile_by_key(
643
946
  * @returns {void}
644
947
  */
645
948
  function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed, render_empty) {
646
- var state = block.s;
647
-
648
- // Variables used in conditional branches - declare with initial values
649
- /** @type {number} */
650
- var a_start = 0;
651
- /** @type {number} */
652
- var b_start = 0;
653
- /** @type {number} */
654
- var a_left = 0;
655
- /** @type {number} */
656
- var b_left = 0;
657
- /** @type {Int32Array} */
658
- var sources = new Int32Array(0);
659
- /** @type {boolean} */
660
- var moved = false;
661
- /** @type {number} */
662
- var pos = 0;
663
- /** @type {number} */
664
- var patched = 0;
665
- /** @type {number} */
666
- var i = 0;
667
-
668
- if (state === null) {
669
- state = block.s = {
670
- array: [],
671
- blocks: [],
672
- keys: null,
673
- empty: null,
674
- };
675
- }
676
-
677
- var a = state.array;
678
- var a_length = a.length;
949
+ var state = /** @type {ListState} */ (block.s);
950
+ var a_length = state.array.length;
679
951
  var b_length = b.length;
680
952
  var j = 0;
681
953
 
@@ -721,67 +993,132 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
721
993
  return;
722
994
  }
723
995
 
996
+ reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_controlled, is_indexed);
997
+ }
998
+
999
+ /**
1000
+ * Diffs a non-empty list against its previous run. Separate from the empty and
1001
+ * create paths so a first render (a mount, a hydration) never compiles it.
1002
+ * @template V
1003
+ * @param {ListAnchor} anchor
1004
+ * @param {Block} block
1005
+ * @param {V[]} b
1006
+ * @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
+ * @returns {void}
1011
+ */
1012
+ function reconcile_by_ref_diff(anchor, block, b, b_blocks, render_fn, is_controlled, is_indexed) {
1013
+ var state = /** @type {ListState} */ (block.s);
1014
+
1015
+ // Variables used in conditional branches - declare with initial values
1016
+ /** @type {number} */
1017
+ var a_left = 0;
1018
+ /** @type {number} */
1019
+ var b_left = 0;
1020
+ /** @type {Int32Array} */
1021
+ var sources = new Int32Array(0);
1022
+ /** @type {boolean} */
1023
+ var moved = false;
1024
+ /** @type {number} */
1025
+ var pos = 0;
1026
+ /** @type {number} */
1027
+ var patched = 0;
1028
+ /** @type {number} */
1029
+ var i = 0;
1030
+
1031
+ var a = state.array;
1032
+ var a_length = a.length;
1033
+ var b_length = b.length;
1034
+ var j = 0;
724
1035
  var a_blocks = state.blocks;
725
- var a_val = a[j];
726
- var b_val = b[j];
1036
+ var a_start = 0;
1037
+ var b_start = 0;
727
1038
  var a_end = a_length - 1;
728
1039
  var b_end = b_length - 1;
1040
+ var b_val;
729
1041
  var b_block;
730
1042
 
731
- outer: {
732
- while (a_val === b_val) {
733
- a[j] = b_val;
734
- b_block = b_blocks[j] = a_blocks[j];
1043
+ // Match from both ends first, including the two end-crossing cases: an old
1044
+ // item that moved to the far end of the new list, and a run whose ends were
1045
+ // exchanged (a reversal, or a swap of two items). Those complete with plain
1046
+ // moves; only what is left afterwards needs the map and LIS below.
1047
+ while (a_start <= a_end && b_start <= b_end) {
1048
+ if (a[a_start] === b[b_start]) {
1049
+ b_val = b[b_start];
1050
+ b_block = b_blocks[b_start] = a_blocks[a_start];
735
1051
  if (is_indexed) {
736
- update_index(b_block, j);
1052
+ update_index(b_block, b_start);
737
1053
  }
738
- ++j;
739
- if (j > a_end || j > b_end) {
740
- break outer;
741
- }
742
- a_val = a[j];
743
- b_val = b[j];
1054
+ a_start++;
1055
+ b_start++;
1056
+ continue;
744
1057
  }
745
-
746
- a_val = a[a_end];
747
- b_val = b[b_end];
748
-
749
- while (a_val === b_val) {
750
- a[a_end] = b_val;
1058
+ if (a[a_end] === b[b_end]) {
1059
+ b_val = b[b_end];
751
1060
  b_block = b_blocks[b_end] = a_blocks[a_end];
752
1061
  if (is_indexed) {
753
1062
  update_index(b_block, b_end);
754
1063
  }
755
1064
  a_end--;
756
1065
  b_end--;
757
- if (j > a_end || j > b_end) {
758
- break outer;
1066
+ continue;
1067
+ }
1068
+ if (a_start === a_end || a_blocks[a_start].s.start === null) {
1069
+ break;
1070
+ }
1071
+ if (a[a_end] === b[b_start]) {
1072
+ // Last old item is the next new one: move it in front of the old run.
1073
+ b_val = b[b_start];
1074
+ b_block = b_blocks[b_start] = a_blocks[a_end];
1075
+ if (is_indexed) {
1076
+ update_index(b_block, b_start);
759
1077
  }
760
- a_val = a[a_end];
1078
+ move(b_block, /** @type {ChildNode} */ (a_blocks[a_start].s.start));
1079
+ a_end--;
1080
+ b_start++;
1081
+ continue;
1082
+ }
1083
+ if (a[a_start] === b[b_end]) {
1084
+ // First old item is the last new one: move it behind the old run.
761
1085
  b_val = b[b_end];
1086
+ b_block = b_blocks[b_end] = a_blocks[a_start];
1087
+ if (is_indexed) {
1088
+ update_index(b_block, b_end);
1089
+ }
1090
+ move(b_block, block_start(b_blocks, b_end + 1, b_length, anchor));
1091
+ a_start++;
1092
+ b_end--;
1093
+ continue;
762
1094
  }
1095
+ break;
763
1096
  }
764
1097
 
765
1098
  var fast_path_removal = false;
766
1099
 
767
- if (j > a_end) {
768
- if (j <= b_end) {
769
- var insert_target = block_start(a_blocks, a_end + 1, a_length, anchor);
770
- while (j <= b_end) {
771
- b_val = b[j];
772
- b_blocks[j] = create_item(insert_target, b_val, j, render_fn, is_indexed, false);
773
- j++;
1100
+ if (a_start > a_end) {
1101
+ if (b_start <= b_end) {
1102
+ var target_node = block_start(b_blocks, b_end + 1, b_length, anchor);
1103
+ 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
+ );
1112
+ b_start++;
774
1113
  }
775
1114
  }
776
- } else if (j > b_end) {
777
- while (j <= a_end) {
778
- destroy_block(a_blocks[j++]);
1115
+ } else if (b_start > b_end) {
1116
+ while (a_start <= a_end) {
1117
+ destroy_block(a_blocks[a_start++]);
779
1118
  }
780
1119
  } else {
781
- a_start = j;
782
- b_start = j;
783
- a_left = a_end - j + 1;
784
- b_left = b_end - j + 1;
1120
+ a_left = a_end - a_start + 1;
1121
+ b_left = b_end - b_start + 1;
785
1122
  sources = new Int32Array(b_left + 1);
786
1123
  moved = false;
787
1124
  pos = 0;
@@ -793,11 +1130,9 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
793
1130
  // When sizes are small, just loop them through
794
1131
  if (b_length < 4 || (a_left | b_left) < 32) {
795
1132
  for (i = a_start; i <= a_end; ++i) {
796
- a_val = a[i];
797
1133
  if (patched < b_left) {
798
1134
  for (j = b_start; j <= b_end; j++) {
799
- b_val = b[j];
800
- if (a_val === b_val) {
1135
+ if (a[i] === b[j]) {
801
1136
  sources[j - b_start] = i + 1;
802
1137
  if (fast_path_removal) {
803
1138
  fast_path_removal = false;
@@ -810,6 +1145,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
810
1145
  } else {
811
1146
  pos = j;
812
1147
  }
1148
+ b_val = b[j];
813
1149
  b_block = b_blocks[j] = a_blocks[i];
814
1150
  if (is_indexed) {
815
1151
  update_index(b_block, j);
@@ -833,10 +1169,8 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
833
1169
  }
834
1170
 
835
1171
  for (i = a_start; i <= a_end; ++i) {
836
- a_val = a[i];
837
-
838
1172
  if (patched < b_left) {
839
- j = map.get(a_val);
1173
+ j = map.get(a[i]);
840
1174
 
841
1175
  if (j !== undefined) {
842
1176
  if (fast_path_removal) {
@@ -851,9 +1185,10 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
851
1185
  } else {
852
1186
  pos = j;
853
1187
  }
854
- block = b_blocks[j] = a_blocks[i];
1188
+ b_val = b[j];
1189
+ b_block = b_blocks[j] = a_blocks[i];
855
1190
  if (is_indexed) {
856
- update_index(block, j);
1191
+ update_index(b_block, j);
857
1192
  }
858
1193
  ++patched;
859
1194
  } else if (!fast_path_removal) {
@@ -875,6 +1210,24 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
875
1210
  var seq = lis_algorithm(sources);
876
1211
  j = seq.length - 1;
877
1212
 
1213
+ // 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.
1216
+ 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
+ }
1226
+ state.array = b;
1227
+ state.blocks = b_blocks;
1228
+ return;
1229
+ }
1230
+
878
1231
  for (i = b_left - 1; i >= 0; i--) {
879
1232
  if (sources[i] === 0) {
880
1233
  pos = i + b_start;
@@ -885,7 +1238,6 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
885
1238
  b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
886
1239
  } else if (j < 0 || i !== seq[j]) {
887
1240
  pos = i + b_start;
888
- b_val = b[pos];
889
1241
  next_pos = pos + 1;
890
1242
 
891
1243
  var target = block_start(b_blocks, next_pos, b_length, anchor);