@remix-run/ui 0.4.0 → 0.6.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 (195) hide show
  1. package/README.md +74 -1
  2. package/dist/accordion/index.js +2 -2
  3. package/dist/accordion/primitives.d.ts +1 -1
  4. package/dist/accordion/primitives.js.map +1 -1
  5. package/dist/anchor/index.js.map +1 -1
  6. package/dist/animation/animate-layout-mixin.js +1 -1
  7. package/dist/animation/animate-layout-mixin.js.map +1 -1
  8. package/dist/animation/animate-mixins.js +2 -2
  9. package/dist/animation/animate-mixins.js.map +1 -1
  10. package/dist/animation/demos/color-interpolation.js +1 -1
  11. package/dist/animation/demos/color-interpolation.js.map +1 -1
  12. package/dist/animation/demos/drag-release.d.ts +1 -1
  13. package/dist/animation/demos/hold-to-confirm.d.ts +3 -3
  14. package/dist/animation/demos/material-ripple.js +1 -3
  15. package/dist/animation/demos/material-ripple.js.map +1 -1
  16. package/dist/animation/index.js +4 -4
  17. package/dist/animation/spring.js.map +1 -1
  18. package/dist/breadcrumbs/index.js +2 -2
  19. package/dist/button/index.js +25 -25
  20. package/dist/button/index.js.map +1 -1
  21. package/dist/checkbox/index.js +11 -9
  22. package/dist/checkbox/index.js.map +1 -1
  23. package/dist/combobox/index.js +4 -4
  24. package/dist/combobox/index.js.map +1 -1
  25. package/dist/combobox/primitives.d.ts +1 -1
  26. package/dist/combobox/primitives.js +5 -5
  27. package/dist/combobox/primitives.js.map +1 -1
  28. package/dist/dev/refresh.d.ts +1 -0
  29. package/dist/dev/refresh.js +2 -0
  30. package/dist/dev/refresh.js.map +1 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +14 -14
  33. package/dist/input/index.js +9 -9
  34. package/dist/input/index.js.map +1 -1
  35. package/dist/jsx-runtime.js +2 -2
  36. package/dist/listbox/index.js +2 -2
  37. package/dist/menu/hover-aim.js.map +1 -1
  38. package/dist/menu/index.js +3 -3
  39. package/dist/menu/primitives.d.ts +1 -1
  40. package/dist/menu/primitives.js +6 -6
  41. package/dist/menu/primitives.js.map +1 -1
  42. package/dist/popover/index.js +3 -3
  43. package/dist/popover/scroll-lock.js.map +1 -1
  44. package/dist/radio/index.js +7 -7
  45. package/dist/radio/index.js.map +1 -1
  46. package/dist/runtime/client-entry-boundary.d.ts +14 -0
  47. package/dist/runtime/client-entry-boundary.js +29 -0
  48. package/dist/runtime/client-entry-boundary.js.map +1 -0
  49. package/dist/runtime/component.d.ts +19 -4
  50. package/dist/runtime/component.js +39 -9
  51. package/dist/runtime/component.js.map +1 -1
  52. package/dist/runtime/core/attributes.js +36 -3
  53. package/dist/runtime/core/attributes.js.map +1 -1
  54. package/dist/runtime/core/children.d.ts +2 -0
  55. package/dist/runtime/core/children.js +26 -2
  56. package/dist/runtime/core/children.js.map +1 -1
  57. package/dist/runtime/core/props.js +2 -2
  58. package/dist/runtime/core/vnode.d.ts +3 -3
  59. package/dist/runtime/core/vnode.js +22 -1
  60. package/dist/runtime/core/vnode.js.map +1 -1
  61. package/dist/runtime/create-element.d.ts +2 -2
  62. package/dist/runtime/create-element.js +3 -2
  63. package/dist/runtime/create-element.js.map +1 -1
  64. package/dist/runtime/diff-dom.js +307 -220
  65. package/dist/runtime/diff-dom.js.map +1 -1
  66. package/dist/runtime/diff-props.js +3 -3
  67. package/dist/runtime/dom.d.ts +12 -0
  68. package/dist/runtime/element-function.d.ts +3 -0
  69. package/dist/runtime/element-function.js +2 -0
  70. package/dist/runtime/element-function.js.map +1 -0
  71. package/dist/runtime/form-navigation.d.ts +25 -0
  72. package/dist/runtime/form-navigation.js +132 -0
  73. package/dist/runtime/form-navigation.js.map +1 -0
  74. package/dist/runtime/frame-resolution.d.ts +5 -0
  75. package/dist/runtime/frame-resolution.js +10 -0
  76. package/dist/runtime/frame-resolution.js.map +1 -0
  77. package/dist/runtime/frame.d.ts +59 -15
  78. package/dist/runtime/frame.js +383 -114
  79. package/dist/runtime/frame.js.map +1 -1
  80. package/dist/runtime/jsx.d.ts +1 -1
  81. package/dist/runtime/jsx.js +1 -1
  82. package/dist/runtime/jsx.js.map +1 -1
  83. package/dist/runtime/key.d.ts +1 -0
  84. package/dist/runtime/key.js +2 -0
  85. package/dist/runtime/key.js.map +1 -0
  86. package/dist/runtime/mixins/attrs-mixin.js +2 -2
  87. package/dist/runtime/mixins/link-mixin.js +4 -3
  88. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  89. package/dist/runtime/mixins/mixin.d.ts +15 -10
  90. package/dist/runtime/mixins/mixin.js +4 -4
  91. package/dist/runtime/mixins/mixin.js.map +1 -1
  92. package/dist/runtime/mixins/on-mixin.js +1 -1
  93. package/dist/runtime/mixins/ref-mixin.js +1 -1
  94. package/dist/runtime/module-preloader.d.ts +8 -0
  95. package/dist/runtime/module-preloader.js +95 -0
  96. package/dist/runtime/module-preloader.js.map +1 -0
  97. package/dist/runtime/navigation.d.ts +4 -1
  98. package/dist/runtime/navigation.js +157 -32
  99. package/dist/runtime/navigation.js.map +1 -1
  100. package/dist/runtime/reconcile.d.ts +11 -10
  101. package/dist/runtime/reconcile.js +549 -421
  102. package/dist/runtime/reconcile.js.map +1 -1
  103. package/dist/runtime/refresh.d.ts +28 -0
  104. package/dist/runtime/refresh.js +31 -0
  105. package/dist/runtime/refresh.js.map +1 -0
  106. package/dist/runtime/render.js +1 -1
  107. package/dist/runtime/render.js.map +1 -1
  108. package/dist/runtime/run.d.ts +3 -1
  109. package/dist/runtime/run.js +15 -7
  110. package/dist/runtime/run.js.map +1 -1
  111. package/dist/runtime/scheduler.js +8 -16
  112. package/dist/runtime/scheduler.js.map +1 -1
  113. package/dist/runtime/to-vnode.d.ts +2 -2
  114. package/dist/runtime/to-vnode.js +68 -20
  115. package/dist/runtime/to-vnode.js.map +1 -1
  116. package/dist/runtime/vdom.d.ts +4 -2
  117. package/dist/runtime/vdom.js +87 -42
  118. package/dist/runtime/vdom.js.map +1 -1
  119. package/dist/runtime/vnode.d.ts +138 -67
  120. package/dist/runtime/vnode.js +17 -12
  121. package/dist/runtime/vnode.js.map +1 -1
  122. package/dist/select/index.js +11 -11
  123. package/dist/select/index.js.map +1 -1
  124. package/dist/select/primitives.d.ts +1 -1
  125. package/dist/select/primitives.js +6 -6
  126. package/dist/server/stream.d.ts +2 -0
  127. package/dist/server/stream.js +74 -29
  128. package/dist/server/stream.js.map +1 -1
  129. package/dist/shared/focus-styles.d.ts +1 -1
  130. package/dist/shared/focus-styles.js +1 -1
  131. package/dist/shared/focus-styles.js.map +1 -1
  132. package/dist/shared/listbox-popover-styles.js +1 -1
  133. package/dist/shared/style-values.js +26 -26
  134. package/dist/shared/style-values.js.map +1 -1
  135. package/dist/style/css-mixin.js +3 -3
  136. package/dist/style/index.js +2 -2
  137. package/dist/style/style.js +25 -6
  138. package/dist/style/style.js.map +1 -1
  139. package/dist/style/stylesheet.d.ts +0 -1
  140. package/dist/style/stylesheet.js +13 -54
  141. package/dist/style/stylesheet.js.map +1 -1
  142. package/dist/tabs/index.js +11 -11
  143. package/dist/tabs/index.js.map +1 -1
  144. package/dist/tabs/primitives.d.ts +1 -1
  145. package/dist/test.js +1 -1
  146. package/dist/toggle/index.js +9 -9
  147. package/dist/toggle/index.js.map +1 -1
  148. package/dist/toggle/primitives.d.ts +1 -1
  149. package/package.json +10 -7
  150. package/src/animation/animate-mixins.ts +3 -3
  151. package/src/animation/demos/color-interpolation.tsx +1 -1
  152. package/src/button/index.ts +24 -24
  153. package/src/checkbox/index.ts +10 -8
  154. package/src/combobox/index.tsx +1 -1
  155. package/src/dev/refresh.ts +1 -0
  156. package/src/index.ts +1 -1
  157. package/src/input/index.ts +8 -8
  158. package/src/radio/index.ts +6 -5
  159. package/src/runtime/client-entry-boundary.ts +54 -0
  160. package/src/runtime/component.ts +65 -16
  161. package/src/runtime/core/attributes.ts +35 -1
  162. package/src/runtime/core/children.ts +24 -2
  163. package/src/runtime/core/vnode.ts +25 -4
  164. package/src/runtime/create-element.ts +6 -5
  165. package/src/runtime/demos/readme.demo.tsx +4 -4
  166. package/src/runtime/diff-dom.ts +370 -244
  167. package/src/runtime/dom.ts +331 -214
  168. package/src/runtime/element-function.ts +1 -0
  169. package/src/runtime/event-listeners.ts +1 -1
  170. package/src/runtime/form-navigation.ts +184 -0
  171. package/src/runtime/frame-resolution.ts +13 -0
  172. package/src/runtime/frame.ts +500 -124
  173. package/src/runtime/jsx.ts +3 -5
  174. package/src/runtime/key.ts +1 -0
  175. package/src/runtime/mixins/link-mixin.ts +1 -0
  176. package/src/runtime/mixins/mixin.ts +21 -18
  177. package/src/runtime/module-preloader.ts +115 -0
  178. package/src/runtime/navigation.ts +204 -30
  179. package/src/runtime/reconcile.ts +762 -759
  180. package/src/runtime/refresh.ts +46 -0
  181. package/src/runtime/run.ts +12 -2
  182. package/src/runtime/scheduler.ts +9 -29
  183. package/src/runtime/to-vnode.ts +85 -23
  184. package/src/runtime/vdom.ts +89 -70
  185. package/src/runtime/vnode.ts +192 -88
  186. package/src/select/index.tsx +9 -9
  187. package/src/server/README.md +13 -0
  188. package/src/server/stream.ts +93 -30
  189. package/src/shared/focus-styles.ts +1 -1
  190. package/src/shared/style-values.ts +26 -26
  191. package/src/style/style.ts +25 -6
  192. package/src/style/stylesheet.ts +32 -57
  193. package/src/tabs/index.tsx +12 -11
  194. package/src/test/README.md +151 -60
  195. package/src/toggle/index.tsx +9 -7
@@ -1,33 +1,43 @@
1
1
  import type { Component, ComponentHandle, FrameContent, FrameHandle } from './component.ts'
2
- import { createComponent, Frame } from './component.ts'
3
- import type { Frame as FrameInstance, FrameRuntime } from './frame.ts'
4
- import { createFrame } from './frame.ts'
2
+ import { createComponent } from './component.ts'
3
+ import type { FrameRuntime } from './frame.ts'
4
+ import { createFrame, isFrameRuntime } from './frame.ts'
5
+ import { unwrapFrameResolution } from './frame-resolution.ts'
5
6
  import { createRangeRoot } from './vdom.ts'
6
7
  import type {
8
+ CommittedFragmentNode,
9
+ CommittedFrameNode,
10
+ CommittedNonRenderNode,
7
11
  ComponentNode,
8
12
  CommittedComponentNode,
9
13
  CommittedHostNode,
10
14
  CommittedTextNode,
15
+ ControlledReflectionState,
16
+ DirectEventBinding,
17
+ DirectEventState,
18
+ FrameNode,
11
19
  HostNode,
20
+ MountingComponentNode,
21
+ ReconcileContext,
22
+ RuntimeHostProps,
12
23
  TextNode,
13
- VNode,
14
- VNodeType,
24
+ VNodeInput,
25
+ VNodeParent,
26
+ CommittedVNode,
15
27
  } from './vnode.ts'
16
28
  import {
17
29
  isCommittedComponentNode,
18
- isComponentNode,
19
30
  isCommittedHostNode,
20
31
  isCommittedTextNode,
21
32
  isFragmentNode,
22
33
  isHostNode,
23
- isNonRenderNode,
24
- isTextNode,
25
34
  findContextFromAncestry,
26
35
  } from './vnode.ts'
27
36
  import { invariant } from './invariant.ts'
28
37
  import { patchHostProps } from './core/props.ts'
29
38
  import type { StyleManager } from '../style/index.ts'
30
- import type { ElementProps } from './jsx.ts'
39
+ import type { ElementFunction } from './element-function.ts'
40
+ import type { Key } from './key.ts'
31
41
  import { skipComments, logHydrationMismatch } from './client-entries.ts'
32
42
  import type { Scheduler } from './scheduler.ts'
33
43
  import { toVNode } from './to-vnode.ts'
@@ -44,6 +54,8 @@ import {
44
54
  type MixinRuntimeState,
45
55
  } from './mixins/mixin.ts'
46
56
  import { isOnMixinDescriptor, type OnMixinDescriptor } from './mixins/on-mixin.ts'
57
+ import { createComponentErrorEvent } from './error-event.ts'
58
+ import { componentStalenessCheck } from './refresh.ts'
47
59
 
48
60
  const SVG_NS = 'http://www.w3.org/2000/svg'
49
61
 
@@ -54,45 +66,41 @@ let activeSchedulerUpdateParents: ParentNode[] | undefined
54
66
 
55
67
  // Compute SVG context for a node based on its parent and type.
56
68
  // Returns true if the node is within an SVG subtree, false otherwise.
57
- function getSvgContext(vParent: VNode, nodeType: VNodeType): boolean {
69
+ function getSvgContext(vParent: VNodeParent, node: VNodeInput): boolean {
58
70
  // Only host elements (strings) can affect SVG context
59
- if (typeof nodeType === 'string') {
71
+ if (node.kind === 'host') {
60
72
  // svg element creates SVG context
61
- if (nodeType === 'svg') return true
73
+ if (node.type === 'svg') return true
62
74
  // foreignObject switches back to HTML context
63
- if (nodeType === 'foreignObject') return false
75
+ if (node.type === 'foreignObject') return false
64
76
  }
65
77
  // Otherwise inherit from parent
66
- return vParent._svg ?? false
78
+ return vParent._svg
67
79
  }
68
80
 
69
- function getHostProps(node: HostNode | CommittedHostNode): ElementProps {
70
- return node._mixedProps ?? node.props
81
+ function getHostProps(node: HostNode | CommittedHostNode): RuntimeHostProps {
82
+ return '_mixedProps' in node ? node._mixedProps : node.props
71
83
  }
72
84
 
73
85
  function markNodePersistedByMixins(node: CommittedHostNode, domParent: ParentNode, token: number) {
74
- node._persistedByMixins = true
75
- node._persistedParentByMixins = domParent
76
- node._persistedRemovalToken = token
86
+ node._persistence = { parent: domParent, token }
77
87
  persistedMixinNodes.add(node)
78
- bindMixinRuntime(node._mixState as MixinRuntimeState | undefined, undefined)
88
+ bindMixinRuntime(node._mixState, undefined)
79
89
  }
80
90
 
81
91
  function unmarkNodePersistedByMixins(node: CommittedHostNode) {
82
- node._persistedByMixins = false
83
- node._persistedParentByMixins = undefined
84
- node._persistedRemovalToken = undefined
92
+ node._persistence = undefined
85
93
  persistedMixinNodes.delete(node)
86
94
  }
87
95
 
88
96
  function findMatchingPersistedMixinNode(
89
97
  type: string,
90
- key: string | undefined,
98
+ key: Key | undefined,
91
99
  domParent: ParentNode,
92
100
  ): CommittedHostNode | null {
93
101
  if (key == null) return null
94
102
  for (let node of persistedMixinNodes) {
95
- if (node._persistedParentByMixins !== domParent) continue
103
+ if (node._persistence?.parent !== domParent) continue
96
104
  if (node.type !== type) continue
97
105
  if (node.key !== key) continue
98
106
  return node
@@ -100,33 +108,10 @@ function findMatchingPersistedMixinNode(
100
108
  return null
101
109
  }
102
110
 
103
- type ControlledReflectionState = {
104
- disposed: boolean
105
- listenersAttached: boolean
106
- pendingRestoreVersion: number
107
- managesValue: boolean
108
- managesChecked: boolean
109
- hasControlledValue: boolean
110
- controlledValue: unknown
111
- hasControlledChecked: boolean
112
- controlledChecked: unknown
113
- onInput: () => void
114
- onChange: () => void
115
- }
116
-
117
- type DirectEventBinding = {
118
- type: string
119
- handler: (event: Event, signal: AbortSignal) => void | Promise<void>
120
- capture: boolean
121
- stableHandler: ((event: Event) => void) | null
122
- reentry: AbortController | null
123
- }
124
-
125
- type DirectEventState = {
126
- bindings: DirectEventBinding[]
127
- }
128
-
129
111
  const EMPTY_DIRECT_EVENT_DESCRIPTORS: OnMixinDescriptor[] = []
112
+ const EMPTY_COMMITTED_CHILDREN: CommittedVNode[] = []
113
+
114
+ type HydrationCursor = { current: Node | null | undefined }
130
115
 
131
116
  function shouldRestoreControlledReflectionOnInput(
132
117
  node: CommittedHostNode,
@@ -144,7 +129,7 @@ function ensureControlledReflection(
144
129
  node: CommittedHostNode,
145
130
  scheduler: Scheduler,
146
131
  ): ControlledReflectionState {
147
- let existing = node._controlledState as ControlledReflectionState | undefined
132
+ let existing = node._controlledState
148
133
  if (existing) return existing
149
134
 
150
135
  let state: ControlledReflectionState = {
@@ -178,8 +163,8 @@ function ensureControlledReflection(
178
163
  return state
179
164
  }
180
165
 
181
- function syncControlledReflection(node: CommittedHostNode, props: ElementProps): void {
182
- let state = node._controlledState as ControlledReflectionState | undefined
166
+ function syncControlledReflection(node: CommittedHostNode, props: RuntimeHostProps): void {
167
+ let state = node._controlledState
183
168
  if (!state || state.disposed) return
184
169
 
185
170
  state.managesValue = canManageValue(node.type, node._dom)
@@ -191,7 +176,7 @@ function syncControlledReflection(node: CommittedHostNode, props: ElementProps):
191
176
  state.pendingRestoreVersion++
192
177
  }
193
178
 
194
- function shouldTrackControlledReflection(props: ElementProps): boolean {
179
+ function shouldTrackControlledReflection(props: RuntimeHostProps): boolean {
195
180
  return hasControlledValueProp(props) || hasControlledCheckedProp(props)
196
181
  }
197
182
 
@@ -222,7 +207,7 @@ function restoreControlledReflections(
222
207
  }
223
208
 
224
209
  function teardownControlledReflection(node: CommittedHostNode): void {
225
- let state = node._controlledState as ControlledReflectionState | undefined
210
+ let state = node._controlledState
226
211
  if (!state) return
227
212
  state.disposed = true
228
213
  state.pendingRestoreVersion++
@@ -233,16 +218,25 @@ function teardownControlledReflection(node: CommittedHostNode): void {
233
218
  }
234
219
  }
235
220
 
221
+ // See abandonDirectEventListeners: skips removeEventListener for discarded subtrees.
222
+ function abandonControlledReflection(node: CommittedHostNode): void {
223
+ let state = node._controlledState
224
+ if (!state) return
225
+ state.disposed = true
226
+ state.pendingRestoreVersion++
227
+ state.listenersAttached = false
228
+ }
229
+
236
230
  function canManageValue(type: string, element: Element): boolean {
237
231
  if (type === 'progress') return false
238
232
  return canReflectProperty(element, 'value')
239
233
  }
240
234
 
241
- function hasControlledValueProp(props: ElementProps): boolean {
235
+ function hasControlledValueProp(props: RuntimeHostProps): boolean {
242
236
  return 'value' in props && props.value !== undefined
243
237
  }
244
238
 
245
- function hasControlledCheckedProp(props: ElementProps): boolean {
239
+ function hasControlledCheckedProp(props: RuntimeHostProps): boolean {
246
240
  return 'checked' in props && props.checked !== undefined
247
241
  }
248
242
 
@@ -263,29 +257,28 @@ function setPropertyReflection(element: Element, key: string, value: unknown): v
263
257
  element[key] = value == null ? '' : value
264
258
  }
265
259
 
260
+ type ResolvedHostProps = {
261
+ props: RuntimeHostProps
262
+ mixState?: MixinRuntimeState
263
+ directEventDescriptors?: OnMixinDescriptor[]
264
+ }
265
+
266
266
  function resolveNodeMixProps(
267
- node: HostNode,
267
+ node: HostNode | CommittedHostNode,
268
+ parent: VNodeParent,
268
269
  frame: FrameHandle,
269
270
  scheduler: Scheduler,
270
271
  state?: MixinRuntimeState,
271
- ): ElementProps {
272
+ ): ResolvedHostProps {
272
273
  let mix = node.props.mix
273
274
  let directEventDescriptors = resolveDirectEventDescriptors(mix)
274
275
  if (directEventDescriptors) {
275
- if (state) {
276
- teardownMixins(state)
277
- }
278
- node._mixState = undefined
279
- node._mixedProps = node.props
280
- node._directEventDescriptors = directEventDescriptors
281
- return node.props
276
+ if (state) teardownMixins(state)
277
+ return { props: node.props, directEventDescriptors }
282
278
  }
283
279
 
284
- node._directEventDescriptors = undefined
285
280
  if (state == null && (mix == null || (Array.isArray(mix) && mix.length === 0))) {
286
- node._mixState = undefined
287
- node._mixedProps = node.props
288
- return node.props
281
+ return { props: node.props }
289
282
  }
290
283
 
291
284
  let resolved = resolveMixedProps({
@@ -297,17 +290,21 @@ function resolveNodeMixProps(
297
290
  return undefined
298
291
  }
299
292
 
300
- return findContextFromAncestry(node, type as Component)
293
+ return findContextFromAncestry(parent, type)
301
294
  },
302
295
  props: node.props,
303
296
  state,
304
297
  })
305
- node._mixState = resolved.state
298
+ return { props: resolved.props, mixState: resolved.state }
299
+ }
300
+
301
+ function applyResolvedHostProps(node: CommittedHostNode, resolved: ResolvedHostProps): void {
306
302
  node._mixedProps = resolved.props
307
- return resolved.props
303
+ node._mixState = resolved.mixState
304
+ node._directEventDescriptors = resolved.directEventDescriptors
308
305
  }
309
306
 
310
- function resolveDirectEventDescriptors(mix: ElementProps['mix']): OnMixinDescriptor[] | null {
307
+ function resolveDirectEventDescriptors(mix: RuntimeHostProps['mix']): OnMixinDescriptor[] | null {
311
308
  if (!mix) return EMPTY_DIRECT_EVENT_DESCRIPTORS
312
309
  if (!Array.isArray(mix)) {
313
310
  return isOnMixinDescriptor(mix) ? [mix] : null
@@ -317,18 +314,18 @@ function resolveDirectEventDescriptors(mix: ElementProps['mix']): OnMixinDescrip
317
314
  }
318
315
 
319
316
  function areOnMixinDescriptors(descriptors: unknown[]): descriptors is OnMixinDescriptor[] {
320
- for (let item of descriptors) {
321
- if (!isOnMixinDescriptor(item)) return false
317
+ for (let i = 0; i < descriptors.length; i++) {
318
+ if (!isOnMixinDescriptor(descriptors[i])) return false
322
319
  }
323
320
  return true
324
321
  }
325
322
 
326
323
  function enqueueMixinBindingUpdate(
327
- this: MixinRuntimeBinding,
324
+ this: MixinRuntimeBinding<CommittedHostNode>,
328
325
  done: (signal: AbortSignal) => void,
329
326
  ): void {
330
- let node = this.target as CommittedHostNode
331
- let state = node._mixState as MixinRuntimeState | undefined
327
+ let node = this.target
328
+ let state = node._mixState
332
329
  this.scheduler.enqueueWork([
333
330
  () => {
334
331
  if (state?.aborted) {
@@ -338,11 +335,12 @@ function enqueueMixinBindingUpdate(
338
335
 
339
336
  dispatchMixinBeforeUpdate(state)
340
337
  let prevProps = getHostProps(node)
341
- let nextProps = resolveNodeMixProps(node, this.frame, this.scheduler, state)
342
- patchHostProps(prevProps, nextProps, this.node)
343
- if (node._controlledState || shouldTrackControlledReflection(nextProps)) {
338
+ let resolved = resolveNodeMixProps(node, node._parent, this.frame, this.scheduler, state)
339
+ applyResolvedHostProps(node, resolved)
340
+ patchHostProps(prevProps, resolved.props, this.node)
341
+ if (node._controlledState || shouldTrackControlledReflection(resolved.props)) {
344
342
  ensureControlledReflection(node, this.scheduler)
345
- syncControlledReflection(node, nextProps)
343
+ syncControlledReflection(node, resolved.props)
346
344
  }
347
345
 
348
346
  dispatchMixinCommit(state)
@@ -359,12 +357,12 @@ function bindNodeMixRuntime(
359
357
  reclaimed: boolean = false,
360
358
  parent?: ParentNode,
361
359
  ) {
362
- let state = node._mixState as MixinRuntimeState | undefined
360
+ let state = node._mixState
363
361
  bindMixinRuntime(
364
362
  state,
365
363
  {
366
364
  node: node._dom,
367
- parent: parent ?? (node._dom.parentNode as ParentNode),
365
+ parent: parent ?? getRequiredDomParent(node._dom),
368
366
  key: node.key,
369
367
  target: node,
370
368
  frame,
@@ -375,12 +373,18 @@ function bindNodeMixRuntime(
375
373
  )
376
374
  }
377
375
 
378
- function isHeadHostNode(node: HostNode): boolean {
376
+ function isHeadHostNode(node: HostNode | CommittedHostNode): boolean {
379
377
  if (node.type === 'head') return true
380
378
  if (node.type.length !== 4) return false
381
379
  return node.type.toLowerCase() === 'head'
382
380
  }
383
381
 
382
+ function getRequiredDomParent(node: Node): ParentNode {
383
+ let parent = node.parentNode
384
+ invariant(parent, 'Expected mounted host node to have a DOM parent')
385
+ return parent
386
+ }
387
+
384
388
  function getDocumentHead(domParent: ParentNode): HTMLHeadElement | null {
385
389
  if (domParent instanceof Document) {
386
390
  return domParent.head
@@ -391,126 +395,199 @@ function getDocumentHead(domParent: ParentNode): HTMLHeadElement | null {
391
395
  return null
392
396
  }
393
397
 
398
+ function commitNonRenderNode(
399
+ node: VNodeInput & { kind: 'empty' },
400
+ parent: VNodeParent,
401
+ svg: boolean,
402
+ ): CommittedNonRenderNode {
403
+ let committed = node as unknown as CommittedNonRenderNode
404
+ committed._parent = parent
405
+ committed._svg = svg
406
+ return committed
407
+ }
408
+
409
+ function commitTextNode(
410
+ node: TextNode,
411
+ parent: VNodeParent,
412
+ svg: boolean,
413
+ dom: Text,
414
+ ): CommittedTextNode {
415
+ let committed = node as unknown as CommittedTextNode
416
+ committed._parent = parent
417
+ committed._svg = svg
418
+ committed._dom = dom
419
+ return committed
420
+ }
421
+
422
+ function beginFragmentNode(
423
+ node: VNodeInput & { kind: 'fragment' },
424
+ parent: VNodeParent,
425
+ svg: boolean,
426
+ ): CommittedFragmentNode {
427
+ let committed = node as unknown as CommittedFragmentNode
428
+ committed._parent = parent
429
+ committed._svg = svg
430
+ committed._children = EMPTY_COMMITTED_CHILDREN
431
+ return committed
432
+ }
433
+
434
+ function commitHostNode(
435
+ node: HostNode,
436
+ parent: VNodeParent,
437
+ svg: boolean,
438
+ dom: Element,
439
+ resolved: ResolvedHostProps,
440
+ children: CommittedVNode[] = EMPTY_COMMITTED_CHILDREN,
441
+ ): CommittedHostNode {
442
+ let committed = node as unknown as CommittedHostNode
443
+ committed._parent = parent
444
+ committed._svg = svg
445
+ committed._dom = dom
446
+ committed._children = children
447
+ committed._mixedProps = resolved.props
448
+ committed._mixState = resolved.mixState
449
+ committed._directEventDescriptors = resolved.directEventDescriptors
450
+ return committed
451
+ }
452
+
453
+ function beginComponentNode(
454
+ node: ComponentNode,
455
+ parent: VNodeParent,
456
+ svg: boolean,
457
+ handle: ComponentHandle,
458
+ context: ReconcileContext,
459
+ ): MountingComponentNode {
460
+ let mounting = node as unknown as MountingComponentNode
461
+ mounting._parent = parent
462
+ mounting._svg = svg
463
+ mounting._handle = handle
464
+ mounting._context = context
465
+ return mounting
466
+ }
467
+
468
+ function commitComponentNode(
469
+ node: MountingComponentNode,
470
+ content: CommittedVNode,
471
+ ): CommittedComponentNode {
472
+ let committed = node as CommittedComponentNode
473
+ committed._content = content
474
+ return committed
475
+ }
476
+
394
477
  export function diffVNodes(
395
- curr: VNode | null,
396
- next: VNode,
478
+ curr: CommittedVNode | null,
479
+ next: VNodeInput,
397
480
  domParent: ParentNode,
398
- frame: FrameHandle,
399
- scheduler: Scheduler,
400
- styles: StyleManager,
401
- vParent: VNode,
402
- rootTarget: EventTarget,
481
+ vParent: VNodeParent,
482
+ context: ReconcileContext,
403
483
  anchor?: Node,
404
- rootCursor?: Node | null,
405
- ): Node | null | undefined {
406
- next._parent = vParent // set parent for initial render context lookups
407
- next._svg = getSvgContext(vParent, next.type)
408
-
409
- // new
484
+ cursor?: HydrationCursor,
485
+ ): CommittedVNode {
410
486
  if (curr === null) {
411
- return insert(
412
- next,
413
- domParent,
414
- frame,
415
- scheduler,
416
- styles,
417
- vParent,
418
- rootTarget,
419
- anchor,
420
- rootCursor,
421
- )
422
- }
423
-
424
- if (curr.type !== next.type) {
425
- replace(curr, next, domParent, frame, scheduler, styles, vParent, rootTarget, anchor)
426
- return rootCursor
427
- }
428
-
429
- if (isNonRenderNode(curr) && isNonRenderNode(next)) {
430
- return rootCursor
431
- }
432
-
433
- if (isCommittedTextNode(curr) && isTextNode(next)) {
434
- diffText(curr, next, vParent)
435
- return rootCursor
436
- }
437
-
438
- if (isCommittedHostNode(curr) && isHostNode(next)) {
439
- diffHost(curr, next, frame, scheduler, styles, vParent, rootTarget)
440
- return rootCursor
487
+ return insert(next, domParent, vParent, context, anchor, cursor)
441
488
  }
442
489
 
443
- if (isCommittedComponentNode(curr) && isComponentNode(next)) {
444
- diffComponent(curr, next, frame, scheduler, styles, domParent, vParent, rootTarget)
445
- return rootCursor
490
+ if (
491
+ componentStalenessCheck !== null &&
492
+ curr.kind === 'component' &&
493
+ next.kind === 'component' &&
494
+ componentStalenessCheck(curr.type) === true
495
+ ) {
496
+ return replace(curr, next, domParent, vParent, context, anchor)
446
497
  }
447
498
 
448
- if (isFragmentNode(curr) && isFragmentNode(next)) {
449
- diffChildren(
450
- curr._children,
451
- next._children,
452
- domParent,
453
- frame,
454
- scheduler,
455
- styles,
456
- next,
457
- rootTarget,
458
- undefined,
459
- anchor,
460
- )
461
- return rootCursor
499
+ if (curr.kind !== next.kind || curr.type !== next.type) {
500
+ return replace(curr, next, domParent, vParent, context, anchor)
462
501
  }
463
502
 
464
- if (curr.type === Frame && next.type === Frame) {
465
- diffFrame(curr, next, domParent, frame, scheduler, styles, vParent, rootTarget, anchor)
466
- return rootCursor
503
+ switch (next.kind) {
504
+ case 'host': {
505
+ return diffHost(curr as CommittedHostNode, next, vParent, context)
506
+ }
507
+ case 'text': {
508
+ return diffText(curr as CommittedTextNode, next, vParent, getSvgContext(vParent, next))
509
+ }
510
+ case 'empty': {
511
+ return commitNonRenderNode(next, vParent, getSvgContext(vParent, next))
512
+ }
513
+ case 'fragment': {
514
+ let childInputs = next._children
515
+ let committed = beginFragmentNode(next, vParent, getSvgContext(vParent, next))
516
+ committed._children = diffChildren(
517
+ (curr as CommittedFragmentNode)._children,
518
+ childInputs,
519
+ domParent,
520
+ committed,
521
+ context,
522
+ undefined,
523
+ anchor,
524
+ )
525
+ return committed
526
+ }
527
+ case 'frame': {
528
+ return diffFrame(
529
+ curr as CommittedFrameNode,
530
+ next,
531
+ domParent,
532
+ vParent,
533
+ context,
534
+ getSvgContext(vParent, next),
535
+ anchor,
536
+ )
537
+ }
538
+ case 'component': {
539
+ return diffComponent(
540
+ curr as CommittedComponentNode,
541
+ next,
542
+ domParent,
543
+ vParent,
544
+ context,
545
+ getSvgContext(vParent, next),
546
+ anchor,
547
+ cursor,
548
+ )
549
+ }
467
550
  }
468
-
469
- invariant(false, 'Unexpected diff case')
470
551
  }
471
552
 
472
553
  function replace(
473
- curr: VNode,
474
- next: VNode,
554
+ curr: CommittedVNode,
555
+ next: VNodeInput,
475
556
  domParent: ParentNode,
476
- frame: FrameHandle,
477
- scheduler: Scheduler,
478
- styles: StyleManager,
479
- vParent: VNode,
480
- rootTarget: EventTarget,
557
+ vParent: VNodeParent,
558
+ context: ReconcileContext,
481
559
  anchor?: Node,
482
- ) {
560
+ ): CommittedVNode {
483
561
  let currAnchor = findFirstDomAnchor(curr)
484
562
  if (currAnchor && currAnchor.parentNode === domParent) {
485
563
  let replacementAnchor = document.createComment('rmx:replace')
486
564
  domParent.insertBefore(replacementAnchor, currAnchor)
487
565
  try {
488
- remove(curr, domParent, scheduler, styles)
489
- insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)
566
+ remove(curr, domParent, context)
567
+ return insert(next, domParent, vParent, context, replacementAnchor)
490
568
  } finally {
491
569
  replacementAnchor.parentNode?.removeChild(replacementAnchor)
492
570
  }
493
- return
494
571
  }
495
572
 
496
573
  let replacementAnchor = findNextSiblingDomAnchor(curr) ?? anchor
497
- remove(curr, domParent, scheduler, styles)
498
- insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)
574
+ remove(curr, domParent, context)
575
+ return insert(next, domParent, vParent, context, replacementAnchor)
499
576
  }
500
577
 
501
578
  function diffHost(
502
579
  curr: CommittedHostNode,
503
580
  next: HostNode,
504
- frame: FrameHandle,
505
- scheduler: Scheduler,
506
- styles: StyleManager,
507
- vParent: VNode,
508
- rootTarget: EventTarget,
509
- ) {
510
- let mixState = curr._mixState as MixinRuntimeState | undefined
581
+ vParent: VNodeParent,
582
+ context: ReconcileContext,
583
+ ): CommittedHostNode {
584
+ let { frame, scheduler, styles } = context
585
+ let mixState = curr._mixState
511
586
  let currProps = getHostProps(curr)
512
- let nextProps = resolveNodeMixProps(next, frame, scheduler, mixState)
513
- let nextMixState = next._mixState as MixinRuntimeState | undefined
587
+ let resolved = resolveNodeMixProps(next, vParent, frame, scheduler, mixState)
588
+ let childInputs = next._children
589
+ let nextProps = resolved.props
590
+ let nextMixState = resolved.mixState
514
591
  let shouldDispatchMixinLifecycle =
515
592
  (nextMixState?.runners.length ?? 0) > 0 && shouldDispatchInlineMixinLifecycle(curr._dom)
516
593
  if (shouldDispatchMixinLifecycle) {
@@ -521,62 +598,49 @@ function diffHost(
521
598
  if (nextProps.innerHTML != null) {
522
599
  // innerHTML is set, update it if changed
523
600
  if (currProps.innerHTML !== nextProps.innerHTML) {
524
- curr._dom.innerHTML = nextProps.innerHTML as string
601
+ curr._dom.innerHTML = nextProps.innerHTML
525
602
  }
526
603
  } else if (currProps.innerHTML != null) {
527
604
  // innerHTML was removed, clear it before adding children
528
605
  curr._dom.innerHTML = ''
529
606
  }
530
607
 
531
- diffChildren(
532
- curr._children,
533
- next._children,
534
- curr._dom,
535
- frame,
536
- scheduler,
537
- styles,
538
- next,
539
- rootTarget,
540
- )
608
+ let committed = commitHostNode(next, vParent, getSvgContext(vParent, next), curr._dom, resolved)
609
+ committed._directEventState = curr._directEventState
610
+ committed._controlledState = curr._controlledState
611
+ committed._children = diffChildren(curr._children, childInputs, curr._dom, committed, context)
541
612
  patchHostProps(currProps, nextProps, curr._dom)
542
613
 
543
- next._dom = curr._dom
544
- next._parent = vParent
545
- next._controller = curr._controller
546
- next._directEventState = curr._directEventState
547
- next._controlledState = curr._controlledState
548
- syncDirectEventListeners(next as CommittedHostNode)
614
+ syncDirectEventListeners(committed)
549
615
 
550
- if (next._controlledState || shouldTrackControlledReflection(nextProps)) {
551
- ensureControlledReflection(next as CommittedHostNode, scheduler)
552
- syncControlledReflection(next as CommittedHostNode, nextProps)
616
+ if (committed._controlledState || shouldTrackControlledReflection(nextProps)) {
617
+ ensureControlledReflection(committed, scheduler)
618
+ syncControlledReflection(committed, nextProps)
553
619
  }
554
620
 
555
- if (next._mixState) {
556
- bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)
621
+ if (committed._mixState) {
622
+ bindNodeMixRuntime(committed, frame, scheduler, styles)
557
623
  }
558
624
  if (shouldDispatchMixinLifecycle) {
559
625
  scheduler.enqueueCommitPhase([() => dispatchMixinCommit(nextMixState)])
560
626
  }
561
627
 
562
- return
628
+ return committed
563
629
  }
564
630
 
565
- function setupHostNode(node: HostNode, dom: Element, scheduler: Scheduler): void {
566
- node._dom = dom
631
+ function setupHostNode(node: CommittedHostNode, scheduler: Scheduler): void {
567
632
  let props = getHostProps(node)
568
- let committedNode = node as CommittedHostNode
569
633
 
570
- syncDirectEventListeners(committedNode)
634
+ syncDirectEventListeners(node)
571
635
 
572
636
  if (shouldTrackControlledReflection(props)) {
573
- ensureControlledReflection(committedNode, scheduler)
574
- syncControlledReflection(committedNode, props)
637
+ ensureControlledReflection(node, scheduler)
638
+ syncControlledReflection(node, props)
575
639
  }
576
640
  }
577
641
 
578
642
  function syncDirectEventListeners(node: CommittedHostNode): void {
579
- let descriptors = node._directEventDescriptors as OnMixinDescriptor[] | undefined
643
+ let descriptors = node._directEventDescriptors
580
644
  if (!descriptors) {
581
645
  teardownDirectEventListeners(node)
582
646
  return
@@ -586,7 +650,7 @@ function syncDirectEventListeners(node: CommittedHostNode): void {
586
650
  return
587
651
  }
588
652
 
589
- let state = node._directEventState as DirectEventState | undefined
653
+ let state = node._directEventState
590
654
  if (!state) {
591
655
  state = { bindings: [] }
592
656
  node._directEventState = state
@@ -594,8 +658,12 @@ function syncDirectEventListeners(node: CommittedHostNode): void {
594
658
 
595
659
  let bindings = state.bindings
596
660
  for (let index = 0; index < descriptors.length; index++) {
597
- let descriptor = descriptors[index]
598
- let [type, handler, captureBoolean = false] = descriptor.args
661
+ // Indexed access instead of array destructuring: destructuring goes through
662
+ // the iterator protocol and allocates on every host-node update.
663
+ let args = descriptors[index].args
664
+ let type = args[0]
665
+ let handler = args[1]
666
+ let captureBoolean = args[2] ?? false
599
667
  let binding = bindings[index]
600
668
 
601
669
  if (!binding) {
@@ -659,7 +727,7 @@ function removeDirectEventBinding(dom: Element, binding: DirectEventBinding): vo
659
727
  }
660
728
 
661
729
  function teardownDirectEventListeners(node: CommittedHostNode): void {
662
- let state = node._directEventState as DirectEventState | undefined
730
+ let state = node._directEventState
663
731
  if (!state) return
664
732
 
665
733
  for (let binding of state.bindings) {
@@ -670,38 +738,56 @@ function teardownDirectEventListeners(node: CommittedHostNode): void {
670
738
  node._directEventState = undefined
671
739
  }
672
740
 
741
+ // Teardown for a node whose DOM subtree is being discarded wholesale: abort
742
+ // pending handler work but skip removeEventListener — the listeners die with
743
+ // the detached node, and removing them one-by-one dominates large teardowns.
744
+ function abandonDirectEventListeners(node: CommittedHostNode): void {
745
+ let state = node._directEventState
746
+ if (!state) return
747
+
748
+ for (let binding of state.bindings) {
749
+ binding.reentry?.abort(new DOMException('', 'AbortError'))
750
+ binding.reentry = null
751
+ }
752
+
753
+ state.bindings.length = 0
754
+ node._directEventState = undefined
755
+ }
756
+
673
757
  function invokeDirectEventBinding(binding: DirectEventBinding, event: Event): void {
674
758
  binding.reentry?.abort(new DOMException('', 'EventReentry'))
675
759
  binding.reentry = new AbortController()
676
760
  void binding.handler(event, binding.reentry.signal)
677
761
  }
678
762
 
679
- function diffText(curr: CommittedTextNode, next: TextNode, vParent: VNode) {
763
+ function diffText(
764
+ curr: CommittedTextNode,
765
+ next: TextNode,
766
+ vParent: VNodeParent,
767
+ svg: boolean,
768
+ ): CommittedTextNode {
680
769
  if (curr._text !== next._text) {
681
770
  curr._dom.textContent = next._text
682
771
  }
683
- next._dom = curr._dom
684
- next._parent = vParent
772
+ return commitTextNode(next, vParent, svg, curr._dom)
685
773
  }
686
774
 
687
775
  function insert(
688
- node: VNode,
776
+ node: VNodeInput,
689
777
  domParent: ParentNode,
690
- frame: FrameHandle,
691
- scheduler: Scheduler,
692
- styles: StyleManager,
693
- vParent: VNode,
694
- rootTarget: EventTarget,
778
+ vParent: VNodeParent,
779
+ context: ReconcileContext,
695
780
  anchor?: Node,
696
- cursor?: Node | null,
697
- ): Node | null | undefined {
698
- node._parent = vParent // set parent for initial render context lookups
699
- node._svg = getSvgContext(vParent, node.type)
781
+ cursor?: HydrationCursor,
782
+ ): CommittedVNode {
783
+ let { frame, scheduler, styles } = context
784
+ let svg = getSvgContext(vParent, node)
785
+ let hydrationNode = cursor?.current
700
786
 
701
787
  // Stop hydration if cursor has reached the anchor (end boundary)
702
788
  // Check BEFORE skipComments to prevent escaping range root markers
703
- if (cursor && anchor && cursor === anchor) {
704
- cursor = null
789
+ if (hydrationNode && anchor && hydrationNode === anchor) {
790
+ hydrationNode = null
705
791
  }
706
792
 
707
793
  // Preserve frame-start markers for non-Frame nodes too, so a following <Frame>
@@ -709,297 +795,280 @@ function insert(
709
795
  // claim its rmx:f marker during hydration instead of being re-inserted fresh.
710
796
  // A rmx:f marker always belongs to a <Frame>, so no non-Frame node should
711
797
  // consume one.
712
- cursor = skipCommentsExceptFrameStart(cursor ?? null)
798
+ hydrationNode = skipCommentsExceptFrameStart(hydrationNode ?? null)
713
799
 
714
800
  // Also check after skipComments in case we skipped past the anchor
715
- if (cursor && anchor && cursor === anchor) {
716
- cursor = null
801
+ if (hydrationNode && anchor && hydrationNode === anchor) {
802
+ hydrationNode = null
717
803
  }
804
+ if (cursor) cursor.current = hydrationNode
718
805
 
719
806
  let doInsert = anchor
720
807
  ? (dom: Node) => domParent.insertBefore(dom, anchor)
721
808
  : (dom: Node) => domParent.appendChild(dom)
722
809
 
723
- if (isNonRenderNode(node)) {
724
- return cursor
810
+ if (node.kind === 'empty') {
811
+ return commitNonRenderNode(node, vParent, svg)
725
812
  }
726
813
 
727
- if (isTextNode(node)) {
728
- if (cursor instanceof Text) {
729
- node._parent = vParent
814
+ if (node.kind === 'text') {
815
+ if (hydrationNode instanceof Text) {
730
816
  // Handle text node consolidation: server renders adjacent text as single node
731
817
  // e.g., <span>Hello {world}</span> → server: "Hello world", client: ["Hello ", "world"]
732
- if (cursor.data !== node._text) {
733
- if (cursor.data.startsWith(node._text) && node._text.length < cursor.data.length) {
818
+ if (hydrationNode.data !== node._text) {
819
+ if (
820
+ hydrationNode.data.startsWith(node._text) &&
821
+ node._text.length < hydrationNode.data.length
822
+ ) {
734
823
  // Consolidation case: split the text node at the boundary
735
824
  // cursor becomes the first part (node._text), remainder is returned for next vnode
736
- let remainder = cursor.splitText(node._text.length)
737
- node._dom = cursor
738
- return remainder
825
+ let remainder = hydrationNode.splitText(node._text.length)
826
+ if (cursor) cursor.current = remainder
827
+ return commitTextNode(node, vParent, svg, hydrationNode)
739
828
  }
740
829
  // Genuine mismatch - correct it
741
- logHydrationMismatch('text mismatch', cursor.data, node._text)
742
- cursor.data = node._text
830
+ logHydrationMismatch('text mismatch', hydrationNode.data, node._text)
831
+ hydrationNode.data = node._text
743
832
  }
744
- node._dom = cursor
745
- return cursor.nextSibling
833
+ if (cursor) cursor.current = hydrationNode.nextSibling
834
+ return commitTextNode(node, vParent, svg, hydrationNode)
746
835
  }
747
836
  let dom = document.createTextNode(node._text)
748
- node._dom = dom
749
- node._parent = vParent
750
837
  doInsert(dom)
751
- return cursor
838
+ return commitTextNode(node, vParent, svg, dom)
752
839
  }
753
840
 
754
- if (isHostNode(node)) {
755
- let hostProps = resolveNodeMixProps(node, frame, scheduler)
841
+ if (node.kind === 'host') {
842
+ let resolved = resolveNodeMixProps(node, vParent, frame, scheduler)
843
+ let hostProps = resolved.props
844
+ let childInputs = node._children
756
845
 
757
846
  if (isHeadHostNode(node)) {
758
847
  let targetHead = getDocumentHead(domParent)
759
848
  if (targetHead) {
760
- let childCursor = cursor
761
- if (cursor instanceof Element && cursor.tagName.toLowerCase() === 'head') {
762
- childCursor = cursor.firstChild
763
- let nextCursor = cursor.nextSibling
764
- if (cursor !== targetHead) {
765
- while (cursor.firstChild) {
766
- targetHead.appendChild(cursor.firstChild)
849
+ let childCursor = cursor ? { current: hydrationNode } : undefined
850
+ if (hydrationNode instanceof Element && hydrationNode.tagName.toLowerCase() === 'head') {
851
+ if (childCursor) childCursor.current = hydrationNode.firstChild
852
+ let nextCursor = hydrationNode.nextSibling
853
+ if (hydrationNode !== targetHead) {
854
+ while (hydrationNode.firstChild) {
855
+ targetHead.appendChild(hydrationNode.firstChild)
767
856
  }
768
- cursor.remove()
857
+ hydrationNode.remove()
769
858
  }
770
- cursor = nextCursor
859
+ if (cursor) cursor.current = nextCursor
771
860
  }
772
861
 
862
+ let committed = commitHostNode(node, vParent, svg, targetHead, resolved)
773
863
  // Render explicit <head> children directly into document.head.
774
- diffChildren(
864
+ committed._children = diffChildren(
775
865
  null,
776
- node._children,
866
+ childInputs,
777
867
  targetHead,
778
- frame,
779
- scheduler,
780
- styles,
781
- node,
782
- rootTarget,
868
+ committed,
869
+ context,
783
870
  childCursor,
784
871
  )
785
872
  patchHostProps({}, hostProps, targetHead)
786
- setupHostNode(node, targetHead, scheduler)
787
- if (node._mixState) {
788
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
873
+ setupHostNode(committed, scheduler)
874
+ if (committed._mixState) {
875
+ bindNodeMixRuntime(committed, frame, scheduler, styles)
789
876
  }
790
- return cursor
877
+ return committed
791
878
  }
792
879
  }
793
880
 
794
881
  // Check for matching mixin-persisted node that can be reclaimed
795
882
  let persistedNode = findMatchingPersistedMixinNode(node.type, node.key, domParent)
796
883
  if (persistedNode) {
797
- reclaimPersistedMixinNode(persistedNode, node, frame, scheduler, styles, vParent, rootTarget)
798
- return cursor
884
+ return reclaimPersistedMixinNode(persistedNode, node, vParent, context)
799
885
  }
800
886
 
801
- if (cursor instanceof Element) {
887
+ if (hydrationNode instanceof Element) {
802
888
  // SVG elements have case-sensitive tag names (e.g. linearGradient, clipPath)
803
889
  // HTML elements are case-insensitive, so we lowercase for comparison
804
- let cursorTag = node._svg ? cursor.tagName : cursor.tagName.toLowerCase()
890
+ let cursorTag = svg ? hydrationNode.tagName : hydrationNode.tagName.toLowerCase()
805
891
  if (cursorTag === node.type) {
806
- let nextCursor = cursor.nextSibling
807
- patchHostProps({}, hostProps, cursor)
892
+ let nextCursor = hydrationNode.nextSibling
893
+ patchHostProps({}, hostProps, hydrationNode)
894
+ let committed = commitHostNode(node, vParent, svg, hydrationNode, resolved)
808
895
 
809
896
  // Handle innerHTML prop
810
897
  if (hostProps.innerHTML != null) {
811
- cursor.innerHTML = hostProps.innerHTML as string
898
+ hydrationNode.innerHTML = hostProps.innerHTML
812
899
  } else {
813
- let childCursor = cursor.firstChild
900
+ let childCursor = { current: hydrationNode.firstChild }
814
901
  // Ignore excess nodes - browser extensions may inject content
815
- diffChildren(
902
+ committed._children = diffChildren(
816
903
  null,
817
- node._children,
818
- cursor,
819
- frame,
820
- scheduler,
821
- styles,
822
- node,
823
- rootTarget,
904
+ childInputs,
905
+ hydrationNode,
906
+ committed,
907
+ context,
824
908
  childCursor,
825
909
  )
826
910
  }
827
911
 
828
- setupHostNode(node, cursor, scheduler)
829
- if (node._mixState) {
830
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
912
+ setupHostNode(committed, scheduler)
913
+ if (committed._mixState) {
914
+ bindNodeMixRuntime(committed, frame, scheduler, styles)
831
915
  }
832
- return nextCursor
916
+ if (cursor) cursor.current = nextCursor
917
+ return committed
833
918
  } else {
834
919
  // Type mismatch - try single-advance retry to handle browser extension injections
835
920
  // at the start of containers. Skip this node and try the next sibling once.
836
- let nextSibling = skipComments(cursor.nextSibling)
921
+ let nextSibling = skipComments(hydrationNode.nextSibling)
837
922
  if (nextSibling instanceof Element) {
838
- let nextTag = node._svg ? nextSibling.tagName : nextSibling.tagName.toLowerCase()
923
+ let nextTag = svg ? nextSibling.tagName : nextSibling.tagName.toLowerCase()
839
924
  if (nextTag === node.type) {
840
925
  let nextCursor = nextSibling.nextSibling
841
926
  // Found a match after skipping - adopt it and leave skipped node in place
842
927
  patchHostProps({}, hostProps, nextSibling)
928
+ let committed = commitHostNode(node, vParent, svg, nextSibling, resolved)
843
929
 
844
930
  if (hostProps.innerHTML != null) {
845
- nextSibling.innerHTML = hostProps.innerHTML as string
931
+ nextSibling.innerHTML = hostProps.innerHTML
846
932
  } else {
847
- let childCursor = nextSibling.firstChild
848
- diffChildren(
933
+ let childCursor = { current: nextSibling.firstChild }
934
+ committed._children = diffChildren(
849
935
  null,
850
- node._children,
936
+ childInputs,
851
937
  nextSibling,
852
- frame,
853
- scheduler,
854
- styles,
855
- node,
856
- rootTarget,
938
+ committed,
939
+ context,
857
940
  childCursor,
858
941
  )
859
942
  }
860
943
 
861
- setupHostNode(node, nextSibling, scheduler)
862
- if (node._mixState) {
863
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
944
+ setupHostNode(committed, scheduler)
945
+ if (committed._mixState) {
946
+ bindNodeMixRuntime(committed, frame, scheduler, styles)
864
947
  }
865
- return nextCursor
948
+ if (cursor) cursor.current = nextCursor
949
+ return committed
866
950
  }
867
951
  }
868
952
  // Retry failed - log mismatch and create new element (don't remove mismatched nodes)
869
953
  logHydrationMismatch('tag', cursorTag, node.type)
870
- cursor = undefined // stop hydration for this tree
954
+ if (cursor) cursor.current = undefined // stop hydration for this tree
871
955
  }
872
956
  }
873
- let dom = node._svg
874
- ? document.createElementNS(SVG_NS, node.type)
875
- : document.createElement(node.type)
957
+ let dom = svg ? document.createElementNS(SVG_NS, node.type) : document.createElement(node.type)
876
958
  patchHostProps({}, hostProps, dom)
959
+ let committed = commitHostNode(node, vParent, svg, dom, resolved)
877
960
 
878
961
  // Handle innerHTML prop
879
962
  if (hostProps.innerHTML != null) {
880
- dom.innerHTML = hostProps.innerHTML as string
963
+ dom.innerHTML = hostProps.innerHTML
881
964
  } else {
882
- diffChildren(null, node._children, dom, frame, scheduler, styles, node, rootTarget)
965
+ committed._children = diffChildren(null, childInputs, dom, committed, context)
883
966
  }
884
967
 
885
- setupHostNode(node, dom, scheduler)
886
- if (node._mixState) {
887
- bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)
968
+ setupHostNode(committed, scheduler)
969
+ if (committed._mixState) {
970
+ bindNodeMixRuntime(committed, frame, scheduler, styles, false, domParent)
888
971
  }
889
972
  doInsert(dom)
890
- return cursor
973
+ return committed
891
974
  }
892
975
 
893
- if (isFragmentNode(node)) {
976
+ if (node.kind === 'fragment') {
977
+ let childInputs = node._children
978
+ let committed = beginFragmentNode(node, vParent, svg)
979
+ let children: Array<VNodeInput | CommittedVNode> = childInputs
894
980
  // Insert fragment children in order before the same anchor
895
- for (let child of node._children) {
896
- cursor = insert(child, domParent, frame, scheduler, styles, node, rootTarget, anchor, cursor)
981
+ for (let i = 0; i < childInputs.length; i++) {
982
+ children[i] = insert(childInputs[i], domParent, committed, context, anchor, cursor)
897
983
  }
898
- return cursor
984
+ committed._children = children as CommittedVNode[]
985
+ return committed
899
986
  }
900
987
 
901
- if (isComponentNode(node)) {
902
- return diffComponent(
903
- null,
904
- node,
905
- frame,
906
- scheduler,
907
- styles,
908
- domParent,
909
- vParent,
910
- rootTarget,
911
- anchor,
912
- cursor,
913
- )
988
+ if (node.kind === 'component') {
989
+ return diffComponent(null, node, domParent, vParent, context, svg, anchor, cursor)
914
990
  }
915
991
 
916
- if (node.type === Frame) {
917
- return insertFrame(
918
- node,
919
- domParent,
920
- frame,
921
- scheduler,
922
- styles,
923
- vParent,
924
- rootTarget,
925
- anchor,
926
- cursor,
927
- )
992
+ if (node.kind === 'frame') {
993
+ return insertFrame(node, domParent, vParent, context, svg, anchor, cursor)
928
994
  }
929
995
 
930
996
  invariant(false, 'Unexpected node type')
931
997
  }
932
998
 
933
999
  function diffFrame(
934
- curr: VNode,
935
- next: VNode,
1000
+ curr: CommittedFrameNode,
1001
+ next: FrameNode,
936
1002
  domParent: ParentNode,
937
- frame: FrameHandle,
938
- scheduler: Scheduler,
939
- styles: StyleManager,
940
- vParent: VNode,
941
- rootTarget: EventTarget,
1003
+ vParent: VNodeParent,
1004
+ context: ReconcileContext,
1005
+ svg: boolean,
942
1006
  anchor?: Node,
943
- ): void {
1007
+ ): CommittedFrameNode {
1008
+ let { frame } = context
944
1009
  let currSrc = getFrameSrc(curr)
945
1010
  let nextSrc = getFrameSrc(next)
946
1011
  let currName = getFrameName(curr)
947
1012
  let nextName = getFrameName(next)
948
1013
 
949
1014
  if (currName !== nextName) {
950
- let replaceAnchor = curr._rangeEnd?.nextSibling ?? anchor
951
- remove(curr, domParent, scheduler, styles)
952
- insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replaceAnchor)
953
- return
1015
+ let replaceAnchor = curr._rangeEnd.nextSibling ?? anchor
1016
+ remove(curr, domParent, context)
1017
+ return insertFrame(next, domParent, vParent, context, svg, replaceAnchor)
954
1018
  }
955
1019
 
956
1020
  // If the frame hasn't resolved yet, preserve existing cancel/remount behavior
957
1021
  // so pending streams from the old src cannot take over the new src.
958
- if (currSrc !== nextSrc && !curr._frameResolved) {
959
- let replaceAnchor = curr._rangeEnd?.nextSibling ?? anchor
960
- remove(curr, domParent, scheduler, styles)
961
- insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replaceAnchor)
962
- return
963
- }
1022
+ if (currSrc !== nextSrc && !curr._state.resolved) {
1023
+ let replaceAnchor = curr._rangeEnd.nextSibling ?? anchor
1024
+ remove(curr, domParent, context)
1025
+ return insertFrame(next, domParent, vParent, context, svg, replaceAnchor)
1026
+ }
1027
+
1028
+ let committed = next as unknown as CommittedFrameNode
1029
+ Object.assign(committed, {
1030
+ _rangeStart: curr._rangeStart,
1031
+ _rangeEnd: curr._rangeEnd,
1032
+ _state: curr._state,
1033
+ _parent: vParent,
1034
+ _svg: svg,
1035
+ })
964
1036
 
965
- next._rangeStart = curr._rangeStart
966
- next._rangeEnd = curr._rangeEnd
967
- next._frameInstance = curr._frameInstance
968
- next._frameFallbackRoot = curr._frameFallbackRoot
969
- next._frameResolveToken = curr._frameResolveToken
970
- next._frameResolved = curr._frameResolved
971
- next._parent = vParent
1037
+ let frameRuntime = getFrameRuntime(frame)
1038
+ let frameInstance = committed._state.instance
1039
+ let serverFrameReload = frameRuntime?.serverFrameReload
972
1040
 
973
1041
  if (currSrc !== nextSrc) {
974
- let frameInstance = next._frameInstance as FrameInstance | undefined
975
1042
  if (frameInstance) {
976
1043
  frameInstance.handle.src = nextSrc
977
1044
  }
978
1045
 
979
- let runtime = getFrameRuntime(frame)
980
- if (runtime) {
981
- resolveClientFrame(next, runtime)
1046
+ if (frameRuntime) {
1047
+ resolveClientFrame(committed, frameRuntime, serverFrameReload)
982
1048
  }
1049
+ } else if (frameRuntime && frameInstance && serverFrameReload) {
1050
+ // Reload client frames that have the same src during a render triggered by an ancestor frame reload
1051
+ resolveClientFrame(committed, frameRuntime, serverFrameReload)
983
1052
  }
984
1053
 
985
- if (!next._frameResolved && next._frameFallbackRoot) {
986
- next._frameFallbackRoot.render(next.props?.fallback ?? null)
1054
+ if (!committed._state.resolved && committed._state.fallbackRoot) {
1055
+ committed._state.fallbackRoot.render(committed.props.fallback ?? null)
987
1056
  }
1057
+ return committed
988
1058
  }
989
1059
 
990
1060
  function insertFrame(
991
- node: VNode,
1061
+ node: FrameNode,
992
1062
  domParent: ParentNode,
993
- frame: FrameHandle,
994
- scheduler: Scheduler,
995
- styles: StyleManager,
996
- vParent: VNode,
997
- rootTarget: EventTarget,
1063
+ vParent: VNodeParent,
1064
+ context: ReconcileContext,
1065
+ svg: boolean,
998
1066
  anchor?: Node,
999
- cursor?: Node | null,
1000
- ): Node | null | undefined {
1067
+ cursor?: HydrationCursor,
1068
+ ): CommittedFrameNode {
1069
+ let { frame, styles } = context
1001
1070
  let runtime = getFrameRuntime(frame)
1002
- if (!runtime || (runtime as { canResolveFrames?: boolean }).canResolveFrames === false) {
1071
+ if (!runtime || runtime.canResolveFrames === false) {
1003
1072
  throw new Error(
1004
1073
  'Cannot render <Frame /> without frame runtime. Use run() or pass frameInit to createRoot/createRangeRoot.',
1005
1074
  )
@@ -1007,33 +1076,23 @@ function insertFrame(
1007
1076
 
1008
1077
  // Hydration path: adopt server-rendered frame markers and reuse the existing
1009
1078
  // frame instance created during createSubFrames().
1010
- if (isFrameStartComment(cursor)) {
1011
- let start = cursor
1079
+ if (isFrameStartComment(cursor?.current)) {
1080
+ let start = cursor.current
1012
1081
  let end = findFrameEndComment(start)
1013
1082
  if (end) {
1014
- node._rangeStart = start
1015
- node._rangeEnd = end
1016
- node._parent = vParent
1017
- node._frameResolveToken = 0
1018
- node._frameResolveController = undefined
1019
- node._frameFallbackRoot = undefined
1020
- node._frameResolved = true
1021
-
1022
- let frameId = getFrameIdFromComment(start)
1023
- let marker = frameId ? runtime.data.f?.[frameId] : undefined
1024
- let src = marker?.src ?? getFrameSrc(node)
1025
1083
  let instance = runtime.frameInstances.get(start)
1084
+ let src = instance?.handle.src ?? getFrameSrc(node)
1026
1085
  if (!instance) {
1027
1086
  instance = createFrame([start, end], {
1028
1087
  name: getFrameName(node),
1029
1088
  src,
1030
- marker: frameId && marker ? { ...marker, id: frameId } : undefined,
1031
1089
  errorTarget: runtime.errorTarget,
1032
1090
  loadModule: runtime.loadModule,
1033
1091
  resolveFrame: runtime.resolveFrame,
1034
1092
  pendingClientEntries: runtime.pendingClientEntries,
1035
1093
  scheduler: runtime.scheduler,
1036
- data: runtime.data,
1094
+ styleManager: runtime.styleManager,
1095
+ data: {},
1037
1096
  moduleCache: runtime.moduleCache,
1038
1097
  moduleLoads: runtime.moduleLoads,
1039
1098
  frameInstances: runtime.frameInstances,
@@ -1042,9 +1101,20 @@ function insertFrame(
1042
1101
  runtime.frameInstances.set(start, instance)
1043
1102
  }
1044
1103
 
1045
- node._frameInstance = instance
1046
-
1047
- return end.nextSibling
1104
+ cursor.current = end.nextSibling
1105
+ return Object.assign(node as unknown as CommittedFrameNode, {
1106
+ _rangeStart: start,
1107
+ _rangeEnd: end,
1108
+ _parent: vParent,
1109
+ _svg: svg,
1110
+ _state: {
1111
+ instance,
1112
+ resolveToken: 0,
1113
+ resolveController: undefined,
1114
+ fallbackRoot: undefined,
1115
+ resolved: true,
1116
+ },
1117
+ })
1048
1118
  }
1049
1119
  }
1050
1120
 
@@ -1057,18 +1127,11 @@ function insertFrame(
1057
1127
  doInsert(start)
1058
1128
  doInsert(end)
1059
1129
 
1060
- node._rangeStart = start
1061
- node._rangeEnd = end
1062
- node._parent = vParent
1063
-
1064
1130
  let fallbackRoot = createRangeRoot([start, end], {
1065
1131
  frame,
1066
1132
  styleManager: styles,
1067
1133
  })
1068
- fallbackRoot.render(node.props?.fallback ?? null)
1069
- node._frameFallbackRoot = fallbackRoot
1070
- node._frameResolved = false
1071
- node._frameResolveToken = 0
1134
+ fallbackRoot.render(node.props.fallback ?? null)
1072
1135
 
1073
1136
  let instance = createFrame([start, end], {
1074
1137
  name: getFrameName(node),
@@ -1078,61 +1141,94 @@ function insertFrame(
1078
1141
  resolveFrame: runtime.resolveFrame,
1079
1142
  pendingClientEntries: runtime.pendingClientEntries,
1080
1143
  scheduler: runtime.scheduler,
1081
- data: runtime.data,
1144
+ styleManager: runtime.styleManager,
1145
+ data: {},
1082
1146
  moduleCache: runtime.moduleCache,
1083
1147
  moduleLoads: runtime.moduleLoads,
1084
1148
  frameInstances: runtime.frameInstances,
1085
1149
  namedFrames: runtime.namedFrames,
1086
1150
  })
1087
- node._frameInstance = instance
1088
1151
  runtime.frameInstances.set(start, instance)
1089
1152
 
1090
- resolveClientFrame(node, runtime)
1153
+ let committed = Object.assign(node as unknown as CommittedFrameNode, {
1154
+ _rangeStart: start,
1155
+ _rangeEnd: end,
1156
+ _parent: vParent,
1157
+ _svg: svg,
1158
+ _state: {
1159
+ instance,
1160
+ fallbackRoot,
1161
+ resolved: false,
1162
+ resolveToken: 0,
1163
+ resolveController: undefined,
1164
+ },
1165
+ })
1166
+ resolveClientFrame(committed, runtime)
1091
1167
 
1092
- return cursor
1168
+ return committed
1093
1169
  }
1094
1170
 
1095
- function resolveClientFrame(node: VNode, runtime: FrameRuntime): void {
1171
+ function resolveClientFrame(
1172
+ node: CommittedFrameNode,
1173
+ runtime: FrameRuntime,
1174
+ serverFrameReload?: { signal: AbortSignal },
1175
+ ): void {
1096
1176
  let frameSrc = getFrameSrc(node)
1097
- let instance = node._frameInstance as FrameInstance | undefined
1098
- if (!instance) return
1099
-
1100
- let token = (node._frameResolveToken ?? 0) + 1
1101
- node._frameResolveToken = token
1102
- node._frameResolveController?.abort()
1103
- let resolveController = new AbortController()
1104
- node._frameResolveController = resolveController
1105
-
1106
- Promise.resolve(runtime.resolveFrame(frameSrc, resolveController.signal, getFrameName(node)))
1107
- .then(async (content) => {
1108
- if (node._frameResolveToken !== token || resolveController.signal.aborted) return
1109
- node._frameFallbackRoot?.dispose()
1110
- node._frameFallbackRoot = undefined
1177
+ let state = node._state
1178
+ let instance = state.instance
1179
+
1180
+ let token = state.resolveToken + 1
1181
+ state.resolveToken = token
1182
+ state.resolveController?.abort()
1183
+ let reload = serverFrameReload
1184
+ ? instance.beginClientFrameReloadForAncestorReload(serverFrameReload.signal)
1185
+ : undefined
1186
+ if (!reload) {
1187
+ instance.cancelReload()
1188
+ }
1189
+ let resolveController = reload?.controller ?? new AbortController()
1190
+ state.resolveController = resolveController
1191
+
1192
+ Promise.resolve()
1193
+ .then(() =>
1194
+ runtime.resolveFrame(frameSrc, {
1195
+ signal: resolveController.signal,
1196
+ target: getFrameName(node),
1197
+ }),
1198
+ )
1199
+ .then(async (resolution) => {
1200
+ if (state.resolveToken !== token || resolveController.signal.aborted) return
1201
+ let { content } = await unwrapFrameResolution(resolution)
1202
+ if (state.resolveToken !== token || resolveController.signal.aborted) return
1203
+ state.fallbackRoot?.dispose()
1204
+ state.fallbackRoot = undefined
1111
1205
  let nextContent = asAbortableFrameContent(content, resolveController.signal)
1112
1206
  await instance.render(nextContent, { signal: resolveController.signal })
1113
- if (node._frameResolveToken !== token || resolveController.signal.aborted) return
1114
- node._frameResolved = true
1207
+ if (state.resolveToken !== token || resolveController.signal.aborted) return
1208
+ state.resolved = true
1209
+ })
1210
+ .catch((error) => {
1211
+ if (reload && state.resolveToken === token && !resolveController.signal.aborted) {
1212
+ runtime.errorTarget.dispatchEvent(createComponentErrorEvent(error))
1213
+ }
1115
1214
  })
1116
- .catch(() => {})
1117
1215
  .finally(() => {
1118
- if (node._frameResolveController === resolveController) {
1119
- node._frameResolveController = undefined
1216
+ reload?.complete()
1217
+ if (state.resolveController === resolveController) {
1218
+ state.resolveController = undefined
1120
1219
  }
1121
1220
  })
1122
1221
  }
1123
1222
 
1124
- function disposeFrameResources(node: VNode): void {
1125
- node._frameResolveToken = (node._frameResolveToken ?? 0) + 1
1126
- node._frameResolveController?.abort()
1127
- node._frameResolveController = undefined
1128
- node._frameFallbackRoot?.dispose()
1129
- node._frameFallbackRoot = undefined
1223
+ function disposeFrameResources(node: CommittedFrameNode): void {
1224
+ let state = node._state
1225
+ state.resolveToken++
1226
+ state.resolveController?.abort()
1227
+ state.resolveController = undefined
1228
+ state.fallbackRoot?.dispose()
1229
+ state.fallbackRoot = undefined
1130
1230
 
1131
- let frameInstance = node._frameInstance as FrameInstance | undefined
1132
- if (frameInstance) {
1133
- frameInstance.dispose()
1134
- node._frameInstance = undefined
1135
- }
1231
+ state.instance.dispose()
1136
1232
  }
1137
1233
 
1138
1234
  function asAbortableFrameContent(content: FrameContent, signal: AbortSignal): FrameContent {
@@ -1163,26 +1259,26 @@ function createAbortableReadableStream(
1163
1259
  }
1164
1260
 
1165
1261
  let removeAbortReadListener: undefined | (() => void)
1166
- let abortRead = new Promise<ReadableStreamReadResult<Uint8Array>>((resolve) => {
1262
+ let abortRead = new Promise<{ done: true; value: undefined }>((resolve) => {
1167
1263
  if (signal.aborted) {
1168
- resolve({ done: true, value: undefined as unknown as Uint8Array })
1264
+ resolve({ done: true, value: undefined })
1169
1265
  return
1170
1266
  }
1171
1267
  let onAbortRead = () => {
1172
- resolve({ done: true, value: undefined as unknown as Uint8Array })
1268
+ resolve({ done: true, value: undefined })
1173
1269
  }
1174
1270
  removeAbortReadListener = () => signal.removeEventListener('abort', onAbortRead)
1175
1271
  signal.addEventListener('abort', onAbortRead, { once: true })
1176
1272
  })
1177
1273
 
1178
- let { done, value } = await Promise.race([reader.read(), abortRead])
1274
+ let result = await Promise.race([reader.read(), abortRead])
1179
1275
  removeAbortReadListener?.()
1180
1276
 
1181
- if (done) {
1277
+ if (result.done) {
1182
1278
  controller.close()
1183
1279
  return
1184
1280
  }
1185
- controller.enqueue(value)
1281
+ controller.enqueue(result.value)
1186
1282
  },
1187
1283
  cancel(reason) {
1188
1284
  signal.removeEventListener('abort', onAbort)
@@ -1191,11 +1287,9 @@ function createAbortableReadableStream(
1191
1287
  })
1192
1288
  }
1193
1289
 
1194
- function removeFrameDomRange(node: VNode, domParent: ParentNode): void {
1290
+ function removeFrameDomRange(node: CommittedFrameNode, domParent: ParentNode): void {
1195
1291
  let start = node._rangeStart
1196
1292
  let end = node._rangeEnd
1197
- if (!(start instanceof Comment) || !(end instanceof Comment)) return
1198
-
1199
1293
  let cursor: Node | null = start
1200
1294
  while (cursor) {
1201
1295
  let nextSibling: Node | null = cursor.nextSibling
@@ -1205,23 +1299,19 @@ function removeFrameDomRange(node: VNode, domParent: ParentNode): void {
1205
1299
  if (cursor === end) break
1206
1300
  cursor = nextSibling
1207
1301
  }
1208
-
1209
- node._rangeStart = undefined
1210
- node._rangeEnd = undefined
1211
1302
  }
1212
1303
 
1213
1304
  function getFrameRuntime(frame: FrameHandle): FrameRuntime | undefined {
1214
- return frame.$runtime as FrameRuntime | undefined
1305
+ let runtime = frame.$runtime
1306
+ return isFrameRuntime(runtime) ? runtime : undefined
1215
1307
  }
1216
1308
 
1217
- function getFrameSrc(node: VNode): string {
1218
- let src = node.props?.src
1219
- invariant(typeof src === 'string' && src.length > 0, '<Frame /> requires a src prop')
1220
- return src
1309
+ function getFrameSrc(node: FrameNode | CommittedFrameNode): string {
1310
+ return node.props.src
1221
1311
  }
1222
1312
 
1223
- function getFrameName(node: VNode): string | undefined {
1224
- let name = node.props?.name
1313
+ function getFrameName(node: FrameNode | CommittedFrameNode): string | undefined {
1314
+ let name = node.props.name
1225
1315
  return typeof name === 'string' && name.length > 0 ? name : undefined
1226
1316
  }
1227
1317
 
@@ -1238,17 +1328,15 @@ function skipCommentsExceptFrameStart(cursor: Node | null): Node | null {
1238
1328
  }
1239
1329
 
1240
1330
  function isFrameStartComment(node: Node | null | undefined): node is Comment {
1241
- return node instanceof Comment && node.data.trim().startsWith('rmx:f:')
1331
+ return isCommentNode(node) && node.data.trim().startsWith('rmx:f:')
1242
1332
  }
1243
1333
 
1244
1334
  function isFrameEndComment(node: Node | null | undefined): node is Comment {
1245
- return node instanceof Comment && node.data.trim() === '/rmx:f'
1335
+ return isCommentNode(node) && node.data.trim() === '/rmx:f'
1246
1336
  }
1247
1337
 
1248
- function getFrameIdFromComment(comment: Comment): string | undefined {
1249
- let text = comment.data.trim()
1250
- if (!text.startsWith('rmx:f:')) return undefined
1251
- return text.slice('rmx:f:'.length)
1338
+ function isCommentNode(node: Node | null | undefined): node is Comment {
1339
+ return node?.nodeType === Node.COMMENT_NODE
1252
1340
  }
1253
1341
 
1254
1342
  function findFrameEndComment(start: Comment): Comment | null {
@@ -1268,72 +1356,53 @@ function findFrameEndComment(start: Comment): Comment | null {
1268
1356
  }
1269
1357
 
1270
1358
  export function renderComponent(
1271
- handle: ComponentHandle,
1272
- currContent: VNode | null,
1273
- next: ComponentNode,
1359
+ currContent: CommittedVNode | null,
1360
+ next: MountingComponentNode | CommittedComponentNode,
1274
1361
  domParent: ParentNode,
1275
- frame: FrameHandle,
1276
- scheduler: Scheduler,
1277
- styles: StyleManager,
1278
- rootTarget: EventTarget,
1279
- vParent?: VNode,
1362
+ context: ReconcileContext,
1280
1363
  anchor?: Node,
1281
- cursor?: Node | null,
1282
- ): Node | null | undefined {
1283
- if (handle.isRemoved()) return cursor
1364
+ cursor?: HydrationCursor,
1365
+ ): CommittedComponentNode {
1366
+ let { scheduler } = context
1367
+ let handle = next._handle
1368
+ if (handle.isRemoved()) {
1369
+ invariant('_content' in next, 'Expected removed component to be committed')
1370
+ return next
1371
+ }
1284
1372
 
1285
1373
  let [element, tasks] = handle.render(next.props)
1286
- let content = toVNode(element)
1287
- let newCursor = diffVNodes(
1288
- currContent,
1289
- content,
1290
- domParent,
1291
- frame,
1292
- scheduler,
1293
- styles,
1294
- next,
1295
- rootTarget,
1296
- anchor,
1297
- cursor,
1298
- )
1299
- next._content = content
1300
- next._handle = handle
1301
- next._parent = vParent
1302
-
1303
- let committed = next as CommittedComponentNode
1304
- handle.setScheduleUpdate(() => {
1305
- scheduler.enqueue(committed, domParent)
1306
- })
1374
+ let content = diffVNodes(currContent, toVNode(element), domParent, next, context, anchor, cursor)
1375
+ let committed = commitComponentNode(next, content)
1376
+ handle.setScheduleUpdate(scheduler, committed, domParent)
1307
1377
 
1308
1378
  scheduler.enqueueTasks(tasks)
1309
1379
 
1310
- return newCursor
1380
+ return committed
1311
1381
  }
1312
1382
 
1313
1383
  function diffComponent(
1314
1384
  curr: CommittedComponentNode | null,
1315
1385
  next: ComponentNode,
1316
- frame: FrameHandle,
1317
- scheduler: Scheduler,
1318
- styles: StyleManager,
1319
1386
  domParent: ParentNode,
1320
- vParent: VNode,
1321
- rootTarget: EventTarget,
1387
+ vParent: VNodeParent,
1388
+ context: ReconcileContext,
1389
+ svg: boolean,
1322
1390
  anchor?: Node,
1323
- cursor?: Node | null,
1324
- ): Node | null | undefined {
1391
+ cursor?: HydrationCursor,
1392
+ ): CommittedComponentNode {
1393
+ let { frame } = context
1325
1394
  if (curr === null) {
1326
- let componentId = vParent._pendingHydrationComponentId
1395
+ let componentId = vParent.kind === 'root' ? vParent._pendingHydrationComponentId : undefined
1327
1396
  if (componentId) {
1328
- vParent._pendingHydrationComponentId = undefined
1397
+ if (vParent.kind === 'root') vParent._pendingHydrationComponentId = undefined
1329
1398
  } else {
1330
1399
  componentId = `c${++idCounter}`
1331
1400
  }
1332
- next._handle = createComponent({
1401
+ let handle = createComponent({
1333
1402
  id: componentId,
1334
1403
  frame,
1335
1404
  type: next.type,
1336
- getContext: (type: Component) => findContextFromAncestry(vParent, type),
1405
+ getContext: (type: ElementFunction) => findContextFromAncestry(vParent, type),
1337
1406
  getFrameByName(name: string) {
1338
1407
  let runtime = getFrameRuntime(frame)
1339
1408
  return runtime?.namedFrames.get(name)
@@ -1343,123 +1412,102 @@ function diffComponent(
1343
1412
  return runtime?.topFrame
1344
1413
  },
1345
1414
  })
1415
+ let mounting = beginComponentNode(next, vParent, svg, handle, context)
1346
1416
 
1347
- return renderComponent(
1348
- next._handle,
1349
- null,
1350
- next,
1351
- domParent,
1352
- frame,
1353
- scheduler,
1354
- styles,
1355
- rootTarget,
1356
- vParent,
1357
- anchor,
1358
- cursor,
1359
- )
1417
+ return renderComponent(null, mounting, domParent, context, anchor, cursor)
1360
1418
  }
1361
- next._handle = curr._handle
1362
- let { _content, _handle } = curr
1363
- return renderComponent(
1364
- _handle,
1365
- _content,
1366
- next,
1367
- domParent,
1368
- frame,
1369
- scheduler,
1370
- styles,
1371
- rootTarget,
1372
- vParent,
1373
- anchor,
1374
- cursor,
1375
- )
1419
+ let mounting = beginComponentNode(next, vParent, svg, curr._handle, context)
1420
+ return renderComponent(curr._content, mounting, domParent, context, anchor, cursor)
1376
1421
  }
1377
1422
 
1378
1423
  // Cleanup without DOM removal - used for descendants when parent DOM node is removed
1379
- function cleanupDescendants(node: VNode, scheduler: Scheduler, styles: StyleManager): void {
1424
+ function cleanupDescendants(node: CommittedVNode, context: ReconcileContext): void {
1425
+ let { scheduler } = context
1380
1426
  if (isCommittedTextNode(node)) {
1381
1427
  return
1382
1428
  }
1383
1429
 
1384
1430
  if (isCommittedHostNode(node)) {
1385
- for (let child of node._children) {
1386
- cleanupDescendants(child, scheduler, styles)
1431
+ let children = node._children
1432
+ for (let i = 0; i < children.length; i++) {
1433
+ cleanupDescendants(children[i], context)
1387
1434
  }
1388
1435
 
1389
- teardownMixins(node._mixState as MixinRuntimeState | undefined)
1390
- teardownDirectEventListeners(node)
1391
- teardownControlledReflection(node)
1392
- if (node._controller) node._controller.abort()
1436
+ teardownMixins(node._mixState)
1437
+ abandonDirectEventListeners(node)
1438
+ abandonControlledReflection(node)
1393
1439
  return
1394
1440
  }
1395
1441
 
1396
1442
  if (isFragmentNode(node)) {
1397
- for (let child of node._children) {
1398
- cleanupDescendants(child, scheduler, styles)
1443
+ let children = node._children
1444
+ for (let i = 0; i < children.length; i++) {
1445
+ cleanupDescendants(children[i], context)
1399
1446
  }
1400
1447
  return
1401
1448
  }
1402
1449
 
1403
1450
  if (isCommittedComponentNode(node)) {
1404
- cleanupDescendants(node._content, scheduler, styles)
1451
+ cleanupDescendants(node._content, context)
1405
1452
  let tasks = node._handle.remove()
1406
1453
  scheduler.enqueueTasks(tasks)
1407
1454
  return
1408
1455
  }
1409
1456
 
1410
- if (node.type === Frame) {
1457
+ if (node.kind === 'frame') {
1411
1458
  disposeFrameResources(node)
1412
1459
  return
1413
1460
  }
1414
1461
  }
1415
1462
 
1416
1463
  export function remove(
1417
- node: VNode,
1464
+ node: CommittedVNode,
1418
1465
  domParent: ParentNode,
1419
- scheduler: Scheduler,
1420
- styles: StyleManager,
1421
- ) {
1466
+ context: ReconcileContext,
1467
+ ): void {
1468
+ let { scheduler } = context
1422
1469
  if (isCommittedTextNode(node)) {
1423
1470
  node._dom.parentNode?.removeChild(node._dom)
1424
1471
  return
1425
1472
  }
1426
1473
 
1427
1474
  if (isCommittedHostNode(node)) {
1428
- if (node._persistedByMixins) return
1475
+ if (node._persistence) return
1429
1476
 
1430
- let persistedRemoval = prepareMixinRemoval(node._mixState as MixinRuntimeState | undefined)
1477
+ let persistedRemoval = prepareMixinRemoval(node._mixState)
1431
1478
  if (persistedRemoval) {
1432
1479
  let token = ++persistedRemovalToken
1433
1480
  markNodePersistedByMixins(node, domParent, token)
1434
1481
  void persistedRemoval
1435
1482
  .catch(() => {})
1436
1483
  .finally(() => {
1437
- if (!node._persistedByMixins) return
1438
- if (node._persistedRemovalToken !== token) return
1484
+ if (!node._persistence) return
1485
+ if (node._persistence.token !== token) return
1439
1486
  unmarkNodePersistedByMixins(node)
1440
- performHostNodeRemoval(node, domParent, scheduler, styles)
1487
+ performHostNodeRemoval(node, domParent, context)
1441
1488
  })
1442
1489
  return
1443
1490
  }
1444
- performHostNodeRemoval(node, domParent, scheduler, styles)
1491
+ performHostNodeRemoval(node, domParent, context)
1445
1492
  return
1446
1493
  }
1447
1494
 
1448
1495
  if (isFragmentNode(node)) {
1449
- for (let child of node._children) {
1450
- remove(child, domParent, scheduler, styles)
1496
+ let children = node._children
1497
+ for (let i = 0; i < children.length; i++) {
1498
+ remove(children[i], domParent, context)
1451
1499
  }
1452
1500
  return
1453
1501
  }
1454
1502
 
1455
1503
  if (isCommittedComponentNode(node)) {
1456
- remove(node._content, domParent, scheduler, styles)
1504
+ remove(node._content, domParent, context)
1457
1505
  let tasks = node._handle.remove()
1458
1506
  scheduler.enqueueTasks(tasks)
1459
1507
  return
1460
1508
  }
1461
1509
 
1462
- if (node.type === Frame) {
1510
+ if (node.kind === 'frame') {
1463
1511
  disposeFrameResources(node)
1464
1512
  removeFrameDomRange(node, domParent)
1465
1513
  return
@@ -1470,63 +1518,53 @@ export function remove(
1470
1518
  function performHostNodeRemoval(
1471
1519
  node: CommittedHostNode,
1472
1520
  domParent: ParentNode,
1473
- scheduler: Scheduler,
1474
- styles: StyleManager,
1475
- ) {
1521
+ context: ReconcileContext,
1522
+ ): void {
1523
+ let children = node._children
1476
1524
  if (isHeadHostNode(node)) {
1477
- for (let child of node._children) {
1478
- remove(child, node._dom, scheduler, styles)
1525
+ for (let i = 0; i < children.length; i++) {
1526
+ remove(children[i], node._dom, context)
1479
1527
  }
1480
1528
  } else {
1481
1529
  // Clean up all descendants first (before removing DOM subtree)
1482
- for (let child of node._children) {
1483
- cleanupDescendants(child, scheduler, styles)
1530
+ for (let i = 0; i < children.length; i++) {
1531
+ cleanupDescendants(children[i], context)
1484
1532
  }
1485
1533
  }
1486
1534
 
1487
- teardownMixins(node._mixState as MixinRuntimeState | undefined)
1488
- teardownDirectEventListeners(node)
1489
- teardownControlledReflection(node)
1490
- // Never remove the real document.head node when reconciling a <head> vnode.
1491
- if (!isHeadHostNode(node)) {
1535
+ teardownMixins(node._mixState)
1536
+ // Never remove the real document.head node when reconciling a <head> vnode,
1537
+ // and since it stays alive, detach its listeners for real.
1538
+ if (isHeadHostNode(node)) {
1539
+ teardownDirectEventListeners(node)
1540
+ teardownControlledReflection(node)
1541
+ } else {
1542
+ abandonDirectEventListeners(node)
1543
+ abandonControlledReflection(node)
1492
1544
  node._dom.parentNode?.removeChild(node._dom)
1493
1545
  }
1494
- if (node._controller) node._controller.abort()
1495
1546
  }
1496
1547
 
1497
1548
  function diffChildren(
1498
- curr: VNode[] | null,
1499
- next: VNode[],
1549
+ curr: CommittedVNode[] | null,
1550
+ next: VNodeInput[],
1500
1551
  domParent: ParentNode,
1501
- frame: FrameHandle,
1502
- scheduler: Scheduler,
1503
- styles: StyleManager,
1504
- vParent: VNode,
1505
- rootTarget: EventTarget,
1506
- cursor?: Node | null,
1552
+ vParent: VNodeParent,
1553
+ context: ReconcileContext,
1554
+ cursor?: HydrationCursor,
1507
1555
  anchor?: Node,
1508
- ) {
1556
+ ): CommittedVNode[] {
1509
1557
  let hasKeys = hasKeyedChildren(next)
1558
+ let committed: Array<VNodeInput | CommittedVNode> = next
1510
1559
 
1511
1560
  if (curr === null) {
1512
1561
  if (hasKeys) {
1513
1562
  warnDuplicateKeys(next)
1514
1563
  }
1515
- for (let node of next) {
1516
- cursor = insert(
1517
- node,
1518
- domParent,
1519
- frame,
1520
- scheduler,
1521
- styles,
1522
- vParent,
1523
- rootTarget,
1524
- anchor,
1525
- cursor,
1526
- )
1564
+ for (let i = 0; i < next.length; i++) {
1565
+ committed[i] = insert(next[i], domParent, vParent, context, anchor, cursor)
1527
1566
  }
1528
- vParent._children = next
1529
- return cursor
1567
+ return committed as CommittedVNode[]
1530
1568
  }
1531
1569
 
1532
1570
  if (
@@ -1535,78 +1573,49 @@ function diffChildren(
1535
1573
  !parentUsesInnerHTML(vParent) &&
1536
1574
  canBulkClearChildren(curr)
1537
1575
  ) {
1538
- for (let node of curr) {
1539
- cleanupDescendants(node, scheduler, styles)
1576
+ for (let i = 0; i < curr.length; i++) {
1577
+ cleanupDescendants(curr[i], context)
1540
1578
  }
1541
1579
  domParent.textContent = ''
1542
- vParent._children = next
1543
- return
1580
+ return EMPTY_COMMITTED_CHILDREN
1544
1581
  }
1545
1582
 
1546
1583
  if (!hasKeys) {
1547
1584
  for (let i = 0; i < next.length; i++) {
1548
1585
  let currentNode = i < curr.length ? curr[i] : null
1549
- diffVNodes(
1550
- currentNode,
1551
- next[i],
1552
- domParent,
1553
- frame,
1554
- scheduler,
1555
- styles,
1556
- vParent,
1557
- rootTarget,
1558
- anchor,
1559
- cursor,
1560
- )
1586
+ committed[i] = diffVNodes(currentNode, next[i], domParent, vParent, context, anchor, cursor)
1561
1587
  }
1562
1588
 
1563
1589
  if (curr.length > next.length) {
1564
1590
  for (let i = next.length; i < curr.length; i++) {
1565
1591
  let node = curr[i]
1566
- if (node) remove(node, domParent, scheduler, styles)
1592
+ if (node) remove(node, domParent, context)
1567
1593
  }
1568
1594
  }
1569
1595
 
1570
- vParent._children = next
1571
- return
1596
+ return committed as CommittedVNode[]
1572
1597
  }
1573
1598
 
1574
- patchKeyedChildren(
1575
- curr,
1576
- next,
1577
- domParent,
1578
- frame,
1579
- scheduler,
1580
- styles,
1581
- vParent,
1582
- rootTarget,
1583
- cursor,
1584
- anchor,
1585
- )
1586
-
1587
- return
1599
+ return patchKeyedChildren(curr, next, domParent, vParent, context, cursor, anchor)
1588
1600
  }
1589
1601
 
1590
- function parentUsesInnerHTML(parent: VNode): boolean {
1602
+ function parentUsesInnerHTML(parent: VNodeParent): boolean {
1591
1603
  return isHostNode(parent) && getHostProps(parent).innerHTML != null
1592
1604
  }
1593
1605
 
1594
- function canBulkClearChildren(children: VNode[]): boolean {
1595
- for (let child of children) {
1596
- if (!canBulkClearNode(child)) return false
1606
+ function canBulkClearChildren(children: CommittedVNode[]): boolean {
1607
+ for (let i = 0; i < children.length; i++) {
1608
+ if (!canBulkClearNode(children[i])) return false
1597
1609
  }
1598
1610
  return true
1599
1611
  }
1600
1612
 
1601
- function canBulkClearNode(node: VNode): boolean {
1613
+ function canBulkClearNode(node: CommittedVNode): boolean {
1602
1614
  if (isCommittedTextNode(node)) return true
1603
1615
 
1604
1616
  if (isCommittedHostNode(node)) {
1605
1617
  if (node._mixState) return false
1606
- for (let child of node._children) {
1607
- if (!canBulkClearNode(child)) return false
1608
- }
1609
- return true
1618
+ return canBulkClearChildren(node._children)
1610
1619
  }
1611
1620
 
1612
1621
  if (isFragmentNode(node)) {
@@ -1620,16 +1629,16 @@ function canBulkClearNode(node: VNode): boolean {
1620
1629
  return false
1621
1630
  }
1622
1631
 
1623
- function hasKeyedChildren(children: VNode[]): boolean {
1624
- for (let node of children) {
1625
- if (node.key != null) return true
1632
+ function hasKeyedChildren(children: Array<{ key?: Key }>): boolean {
1633
+ for (let i = 0; i < children.length; i++) {
1634
+ if (children[i].key != null) return true
1626
1635
  }
1627
1636
  return false
1628
1637
  }
1629
1638
 
1630
- function warnDuplicateKeys(children: VNode[]): void {
1631
- let seenKeys: Set<string> | undefined
1632
- let duplicateKeys: Set<string> | undefined
1639
+ function warnDuplicateKeys(children: Array<{ key?: Key }>): void {
1640
+ let seenKeys: Set<Key> | undefined
1641
+ let duplicateKeys: Set<Key> | undefined
1633
1642
 
1634
1643
  for (let node of children) {
1635
1644
  if (node.key == null) continue
@@ -1648,7 +1657,7 @@ function warnDuplicateKeys(children: VNode[]): void {
1648
1657
  }
1649
1658
 
1650
1659
  if (duplicateKeys?.size) {
1651
- let quotedKeys = Array.from(duplicateKeys, (key) => `"${key}"`)
1660
+ let quotedKeys = Array.from(duplicateKeys, (key) => `"${String(key)}"`)
1652
1661
  console.warn(
1653
1662
  `Duplicate keys detected in siblings: ${quotedKeys.join(', ')}. Keys should be unique.`,
1654
1663
  )
@@ -1656,17 +1665,14 @@ function warnDuplicateKeys(children: VNode[]): void {
1656
1665
  }
1657
1666
 
1658
1667
  function patchKeyedChildren(
1659
- curr: VNode[],
1660
- next: VNode[],
1668
+ curr: CommittedVNode[],
1669
+ next: VNodeInput[],
1661
1670
  domParent: ParentNode,
1662
- frame: FrameHandle,
1663
- scheduler: Scheduler,
1664
- styles: StyleManager,
1665
- vParent: VNode,
1666
- rootTarget: EventTarget,
1667
- cursor?: Node | null,
1671
+ vParent: VNodeParent,
1672
+ context: ReconcileContext,
1673
+ cursor?: HydrationCursor,
1668
1674
  anchor?: Node,
1669
- ): void {
1675
+ ): CommittedVNode[] {
1670
1676
  let matches =
1671
1677
  matchKeyedChildrenInOrder(curr, next) ??
1672
1678
  matchKeyedChildrenAfterSingleRemoval(curr, next) ??
@@ -1675,46 +1681,36 @@ function patchKeyedChildren(
1675
1681
  warnDuplicateKeys(next)
1676
1682
  matches = matchKeyedChildren(curr, next)
1677
1683
  }
1684
+ let committed: Array<VNodeInput | CommittedVNode> = next
1678
1685
 
1679
1686
  let matchAnalysis = analyzeKeyedChildMatches(curr.length, matches)
1680
1687
  if (matchAnalysis.hasRemovals) {
1681
1688
  let usedOldIndexes = new Uint8Array(curr.length)
1682
1689
 
1683
- for (let match of matches) {
1684
- if (match.oldIndex >= 0) {
1685
- usedOldIndexes[match.oldIndex] = 1
1690
+ for (let index = 0; index < matches.length; index++) {
1691
+ let oldIndex = matches[index]
1692
+ if (oldIndex >= 0) {
1693
+ usedOldIndexes[oldIndex] = 1
1686
1694
  }
1687
1695
  }
1688
1696
 
1689
1697
  for (let oldIndex = 0; oldIndex < curr.length; oldIndex++) {
1690
1698
  if (usedOldIndexes[oldIndex] === 0) {
1691
- remove(curr[oldIndex], domParent, scheduler, styles)
1699
+ remove(curr[oldIndex], domParent, context)
1692
1700
  }
1693
1701
  }
1694
1702
  }
1695
1703
 
1696
- vParent._children = next
1697
-
1698
1704
  for (let index = 0; index < next.length; index++) {
1699
- let match = matches[index]
1700
- let oldNode = match.oldIndex >= 0 ? curr[match.oldIndex] : null
1705
+ let oldIndex = matches[index]
1706
+ let oldNode = oldIndex >= 0 ? curr[oldIndex] : null
1701
1707
 
1702
- diffVNodes(
1703
- oldNode,
1704
- next[index],
1705
- domParent,
1706
- frame,
1707
- scheduler,
1708
- styles,
1709
- vParent,
1710
- rootTarget,
1711
- anchor,
1712
- cursor,
1713
- )
1708
+ committed[index] = diffVNodes(oldNode, next[index], domParent, vParent, context, anchor, cursor)
1714
1709
  }
1710
+ let committedChildren = committed as CommittedVNode[]
1715
1711
 
1716
1712
  if (matchAnalysis.canSkipPlacement) {
1717
- return
1713
+ return committedChildren
1718
1714
  }
1719
1715
 
1720
1716
  let stableIndexes = lisMatches(matches)
@@ -1722,7 +1718,7 @@ function patchKeyedChildren(
1722
1718
  let placementAnchor: Node | null = anchor ?? null
1723
1719
 
1724
1720
  for (let index = next.length - 1; index >= 0; index--) {
1725
- let nextNode = next[index]
1721
+ let nextNode = committedChildren[index]
1726
1722
  let isStable = stableIndexes[stableCursor] === index
1727
1723
 
1728
1724
  if (isStable) {
@@ -1733,10 +1729,14 @@ function patchKeyedChildren(
1733
1729
 
1734
1730
  placementAnchor = findFirstDomAnchor(nextNode) ?? placementAnchor
1735
1731
  }
1732
+ return committedChildren
1736
1733
  }
1737
1734
 
1738
- function matchKeyedChildren(curr: VNode[], next: VNode[]): Array<{ oldIndex: number }> {
1739
- let oldKeyMap = new Map<string, number>()
1735
+ // Keyed child matches are arrays of old indexes (-1 = no match / new node),
1736
+ // parallel to `next`. Plain numbers instead of wrapper objects keep large
1737
+ // keyed diffs (1000-row tables) allocation-free.
1738
+ function matchKeyedChildren(curr: CommittedVNode[], next: VNodeInput[]): number[] {
1739
+ let oldKeyMap = new Map<Key, number>()
1740
1740
  let usedOldIndexes = new Set<number>()
1741
1741
  let unkeyedSearchStart = 0
1742
1742
 
@@ -1745,7 +1745,9 @@ function matchKeyedChildren(curr: VNode[], next: VNode[]): Array<{ oldIndex: num
1745
1745
  if (key != null) oldKeyMap.set(key, index)
1746
1746
  }
1747
1747
 
1748
- return next.map((nextNode) => {
1748
+ let matches: number[] = []
1749
+ for (let nextIndex = 0; nextIndex < next.length; nextIndex++) {
1750
+ let nextNode = next[nextIndex]
1749
1751
  let oldIndex = -1
1750
1752
 
1751
1753
  if (nextNode.key != null) {
@@ -1770,16 +1772,15 @@ function matchKeyedChildren(curr: VNode[], next: VNode[]): Array<{ oldIndex: num
1770
1772
  }
1771
1773
 
1772
1774
  if (oldIndex >= 0) usedOldIndexes.add(oldIndex)
1773
- return { oldIndex }
1774
- })
1775
+ matches.push(oldIndex)
1776
+ }
1777
+
1778
+ return matches
1775
1779
  }
1776
1780
 
1777
- function matchKeyedChildrenInOrder(
1778
- curr: VNode[],
1779
- next: VNode[],
1780
- ): Array<{ oldIndex: number }> | null {
1781
+ function matchKeyedChildrenInOrder(curr: CommittedVNode[], next: VNodeInput[]): number[] | null {
1781
1782
  let length = Math.min(curr.length, next.length)
1782
- let matches: Array<{ oldIndex: number }> = []
1783
+ let matches: number[] = []
1783
1784
 
1784
1785
  for (let index = 0; index < length; index++) {
1785
1786
  let nextNode = next[index]
@@ -1790,24 +1791,24 @@ function matchKeyedChildrenInOrder(
1790
1791
  return null
1791
1792
  }
1792
1793
 
1793
- matches.push({ oldIndex: index })
1794
+ matches.push(index)
1794
1795
  }
1795
1796
 
1796
1797
  for (let index = length; index < next.length; index++) {
1797
1798
  if (next[index].key == null) return null
1798
- matches.push({ oldIndex: -1 })
1799
+ matches.push(-1)
1799
1800
  }
1800
1801
 
1801
1802
  return matches
1802
1803
  }
1803
1804
 
1804
1805
  function matchKeyedChildrenAfterSingleRemoval(
1805
- curr: VNode[],
1806
- next: VNode[],
1807
- ): Array<{ oldIndex: number }> | null {
1806
+ curr: CommittedVNode[],
1807
+ next: VNodeInput[],
1808
+ ): number[] | null {
1808
1809
  if (curr.length !== next.length + 1) return null
1809
1810
 
1810
- let matches: Array<{ oldIndex: number }> = []
1811
+ let matches: number[] = []
1811
1812
  let oldIndex = 0
1812
1813
  let skippedOldNode = false
1813
1814
 
@@ -1817,7 +1818,7 @@ function matchKeyedChildrenAfterSingleRemoval(
1817
1818
 
1818
1819
  let oldNode = curr[oldIndex]
1819
1820
  if (oldNode.key === nextNode.key && oldNode.type === nextNode.type) {
1820
- matches.push({ oldIndex })
1821
+ matches.push(oldIndex)
1821
1822
  oldIndex++
1822
1823
  continue
1823
1824
  }
@@ -1831,7 +1832,7 @@ function matchKeyedChildrenAfterSingleRemoval(
1831
1832
  return null
1832
1833
  }
1833
1834
 
1834
- matches.push({ oldIndex })
1835
+ matches.push(oldIndex)
1835
1836
  oldIndex++
1836
1837
  }
1837
1838
 
@@ -1839,12 +1840,12 @@ function matchKeyedChildrenAfterSingleRemoval(
1839
1840
  }
1840
1841
 
1841
1842
  function matchKeyedChildrenAfterPairSwap(
1842
- curr: VNode[],
1843
- next: VNode[],
1844
- ): Array<{ oldIndex: number }> | null {
1843
+ curr: CommittedVNode[],
1844
+ next: VNodeInput[],
1845
+ ): number[] | null {
1845
1846
  if (curr.length !== next.length) return null
1846
1847
 
1847
- let matches: Array<{ oldIndex: number }> = []
1848
+ let matches: number[] = []
1848
1849
  let firstMismatch = -1
1849
1850
  let secondMismatch = -1
1850
1851
 
@@ -1854,7 +1855,7 @@ function matchKeyedChildrenAfterPairSwap(
1854
1855
 
1855
1856
  let oldNode = curr[index]
1856
1857
  if (oldNode.key === nextNode.key && oldNode.type === nextNode.type) {
1857
- matches.push({ oldIndex: index })
1858
+ matches.push(index)
1858
1859
  continue
1859
1860
  }
1860
1861
 
@@ -1865,7 +1866,7 @@ function matchKeyedChildrenAfterPairSwap(
1865
1866
  } else {
1866
1867
  return null
1867
1868
  }
1868
- matches.push({ oldIndex: -1 })
1869
+ matches.push(-1)
1869
1870
  }
1870
1871
 
1871
1872
  if (firstMismatch === -1) return matches
@@ -1885,42 +1886,43 @@ function matchKeyedChildrenAfterPairSwap(
1885
1886
  return null
1886
1887
  }
1887
1888
 
1888
- matches[firstMismatch] = { oldIndex: secondMismatch }
1889
- matches[secondMismatch] = { oldIndex: firstMismatch }
1889
+ matches[firstMismatch] = secondMismatch
1890
+ matches[secondMismatch] = firstMismatch
1890
1891
  return matches
1891
1892
  }
1892
1893
 
1893
1894
  function analyzeKeyedChildMatches(
1894
1895
  currentLength: number,
1895
- matches: ReadonlyArray<{ oldIndex: number }>,
1896
+ matches: readonly number[],
1896
1897
  ): { hasRemovals: boolean; canSkipPlacement: boolean } {
1897
1898
  let hasRemovals = matches.length !== currentLength
1898
1899
  let canSkipPlacement = true
1899
1900
  let lastOldIndex = -1
1900
1901
  let sawNewNode = false
1901
1902
 
1902
- for (let match of matches) {
1903
- if (match.oldIndex < 0) {
1903
+ for (let index = 0; index < matches.length; index++) {
1904
+ let oldIndex = matches[index]
1905
+ if (oldIndex < 0) {
1904
1906
  hasRemovals = true
1905
1907
  sawNewNode = true
1906
1908
  continue
1907
1909
  }
1908
1910
 
1909
- if (sawNewNode || match.oldIndex < lastOldIndex) {
1911
+ if (sawNewNode || oldIndex < lastOldIndex) {
1910
1912
  canSkipPlacement = false
1911
1913
  }
1912
- lastOldIndex = match.oldIndex
1914
+ lastOldIndex = oldIndex
1913
1915
  }
1914
1916
 
1915
1917
  return { hasRemovals, canSkipPlacement }
1916
1918
  }
1917
1919
 
1918
- function lisMatches(matches: ReadonlyArray<{ oldIndex: number }>): number[] {
1920
+ function lisMatches(matches: readonly number[]): number[] {
1919
1921
  let predecessors = Array.from<number>({ length: matches.length })
1920
1922
  let tails: number[] = []
1921
1923
 
1922
1924
  for (let index = 0; index < matches.length; index++) {
1923
- let value = matches[index].oldIndex + 1
1925
+ let value = matches[index] + 1
1924
1926
  if (value === 0) continue
1925
1927
 
1926
1928
  let low = 0
@@ -1929,7 +1931,7 @@ function lisMatches(matches: ReadonlyArray<{ oldIndex: number }>): number[] {
1929
1931
  while (low < high) {
1930
1932
  let middle = (low + high) >> 1
1931
1933
 
1932
- if (matches[tails[middle]].oldIndex + 1 < value) {
1934
+ if (matches[tails[middle]] + 1 < value) {
1933
1935
  low = middle + 1
1934
1936
  } else {
1935
1937
  high = middle
@@ -1950,7 +1952,7 @@ function lisMatches(matches: ReadonlyArray<{ oldIndex: number }>): number[] {
1950
1952
  return tails
1951
1953
  }
1952
1954
 
1953
- function placeVNode(node: VNode, domParent: ParentNode, anchor: Node | null): void {
1955
+ function placeVNode(node: CommittedVNode, domParent: ParentNode, anchor: Node | null): void {
1954
1956
  let firstDom = findFirstDomAnchor(node)
1955
1957
  if (!firstDom || firstDom.parentNode !== domParent) return
1956
1958
 
@@ -1962,27 +1964,28 @@ function placeVNode(node: VNode, domParent: ParentNode, anchor: Node | null): vo
1962
1964
  moveDomRange(domParent, firstDom, lastDom, anchor)
1963
1965
  }
1964
1966
 
1965
- export function findFirstDomAnchor(node: VNode | null | undefined): Node | null {
1967
+ export function findFirstDomAnchor(node: CommittedVNode | null | undefined): Node | null {
1966
1968
  if (!node) return null
1967
1969
  if (isCommittedTextNode(node)) return node._dom
1968
1970
  if (isCommittedHostNode(node)) return node._dom
1969
1971
  if (isCommittedComponentNode(node)) return findFirstDomAnchor(node._content)
1970
- if (node.type === Frame) return node._rangeStart ?? null
1972
+ if (node.kind === 'frame') return node._rangeStart
1971
1973
  if (isFragmentNode(node)) {
1972
- for (let child of node._children) {
1973
- let dom = findFirstDomAnchor(child)
1974
+ let children = node._children
1975
+ for (let i = 0; i < children.length; i++) {
1976
+ let dom = findFirstDomAnchor(children[i])
1974
1977
  if (dom) return dom
1975
1978
  }
1976
1979
  }
1977
1980
  return null
1978
1981
  }
1979
1982
 
1980
- export function findLastDomAnchor(node: VNode | null | undefined): Node | null {
1983
+ export function findLastDomAnchor(node: CommittedVNode | null | undefined): Node | null {
1981
1984
  if (!node) return null
1982
1985
  if (isCommittedTextNode(node)) return node._dom
1983
1986
  if (isCommittedHostNode(node)) return node._dom
1984
1987
  if (isCommittedComponentNode(node)) return findLastDomAnchor(node._content)
1985
- if (node.type === Frame) return node._rangeEnd ?? null
1988
+ if (node.kind === 'frame') return node._rangeEnd
1986
1989
  if (isFragmentNode(node)) {
1987
1990
  for (let i = node._children.length - 1; i >= 0; i--) {
1988
1991
  let dom = findLastDomAnchor(node._children[i])
@@ -2020,20 +2023,27 @@ function shouldDispatchInlineMixinLifecycle(node: Node): boolean {
2020
2023
  let parents = activeSchedulerUpdateParents
2021
2024
  if (!parents?.length) return true
2022
2025
  for (let parent of parents) {
2023
- let parentNode = parent as Node
2026
+ if (!(parent instanceof Node)) continue
2027
+ let parentNode = parent
2024
2028
  if (parentNode === node) return false
2025
2029
  if (parentNode.contains(node)) return false
2026
2030
  }
2027
2031
  return true
2028
2032
  }
2029
2033
 
2030
- export function findNextSiblingDomAnchor(curr: VNode): Node | null {
2034
+ export function findNextSiblingDomAnchor(
2035
+ curr: CommittedVNode | MountingComponentNode,
2036
+ ): Node | null {
2031
2037
  let vParent = curr._parent
2032
- if (!vParent || !Array.isArray(vParent._children)) return null
2038
+ if (vParent.kind === 'component') return findNextSiblingDomAnchor(vParent)
2033
2039
  let children = vParent._children
2034
- if (children.length === 0) return findNextSiblingDomAnchor(vParent)
2040
+ if (children.length === 0) {
2041
+ if (vParent.kind === 'root') return vParent._rangeEnd ?? null
2042
+ if (vParent.kind === 'fragment') return findNextSiblingDomAnchor(vParent)
2043
+ return null
2044
+ }
2035
2045
 
2036
- let idx = children.indexOf(curr)
2046
+ let idx = children.indexOf(curr as CommittedVNode)
2037
2047
  if (idx === -1) return null
2038
2048
  for (let i = idx + 1; i < children.length; i++) {
2039
2049
  let dom = findFirstDomAnchor(children[i])
@@ -2044,60 +2054,53 @@ export function findNextSiblingDomAnchor(curr: VNode): Node | null {
2044
2054
  return findNextSiblingDomAnchor(vParent)
2045
2055
  }
2046
2056
 
2047
- return null
2057
+ return vParent.kind === 'root' ? (vParent._rangeEnd ?? null) : null
2048
2058
  }
2049
2059
 
2050
2060
  function reclaimPersistedMixinNode(
2051
2061
  persistedNode: CommittedHostNode,
2052
2062
  newNode: HostNode,
2053
- frame: FrameHandle,
2054
- scheduler: Scheduler,
2055
- styles: StyleManager,
2056
- vParent: VNode,
2057
- rootTarget: EventTarget,
2058
- ): void {
2059
- cancelPendingMixinRemoval(persistedNode._mixState as MixinRuntimeState | undefined)
2063
+ vParent: VNodeParent,
2064
+ context: ReconcileContext,
2065
+ ): CommittedHostNode {
2066
+ let { frame, scheduler, styles } = context
2067
+ cancelPendingMixinRemoval(persistedNode._mixState)
2060
2068
  unmarkNodePersistedByMixins(persistedNode)
2061
2069
 
2062
- newNode._dom = persistedNode._dom
2063
- newNode._parent = vParent
2064
- newNode._controller = persistedNode._controller
2065
- newNode._mixState = persistedNode._mixState
2066
- newNode._directEventState = persistedNode._directEventState
2067
- newNode._controlledState = persistedNode._controlledState
2068
-
2069
2070
  let prevProps = getHostProps(persistedNode)
2070
- let nextProps = resolveNodeMixProps(
2071
+ let childInputs = newNode._children
2072
+ let resolved = resolveNodeMixProps(newNode, vParent, frame, scheduler, persistedNode._mixState)
2073
+ let nextProps = resolved.props
2074
+ let committed = commitHostNode(
2071
2075
  newNode,
2072
- frame,
2073
- scheduler,
2074
- newNode._mixState as MixinRuntimeState | undefined,
2076
+ vParent,
2077
+ getSvgContext(vParent, newNode),
2078
+ persistedNode._dom,
2079
+ resolved,
2075
2080
  )
2081
+ committed._directEventState = persistedNode._directEventState
2082
+ committed._controlledState = persistedNode._controlledState
2076
2083
  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
2077
- dispatchMixinBeforeUpdate(newNode._mixState as MixinRuntimeState | undefined)
2084
+ dispatchMixinBeforeUpdate(committed._mixState)
2078
2085
  }
2079
2086
  patchHostProps(prevProps, nextProps, persistedNode._dom)
2080
- syncDirectEventListeners(newNode as CommittedHostNode)
2081
- ensureControlledReflection(newNode as CommittedHostNode, scheduler)
2082
- syncControlledReflection(newNode as CommittedHostNode, nextProps)
2087
+ syncDirectEventListeners(committed)
2088
+ ensureControlledReflection(committed, scheduler)
2089
+ syncControlledReflection(committed, nextProps)
2083
2090
 
2084
- diffChildren(
2091
+ committed._children = diffChildren(
2085
2092
  persistedNode._children,
2086
- newNode._children,
2093
+ childInputs,
2087
2094
  persistedNode._dom,
2088
- frame,
2089
- scheduler,
2090
- styles,
2091
- newNode,
2092
- rootTarget,
2095
+ committed,
2096
+ context,
2093
2097
  )
2094
2098
 
2095
- if (newNode._mixState) {
2096
- bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)
2099
+ if (committed._mixState) {
2100
+ bindNodeMixRuntime(committed, frame, scheduler, styles, true)
2097
2101
  }
2098
2102
  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
2099
- scheduler.enqueueCommitPhase([
2100
- () => dispatchMixinCommit(newNode._mixState as MixinRuntimeState | undefined),
2101
- ])
2103
+ scheduler.enqueueCommitPhase([() => dispatchMixinCommit(committed._mixState)])
2102
2104
  }
2105
+ return committed
2103
2106
  }