@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,14 +1,23 @@
1
1
  import { jsx } from './jsx.ts'
2
- import { Frame, createFrameHandle, type FrameContent } from './component.ts'
2
+ import { Frame, createFrameHandle, type FrameContent, type FrameResolution } from './component.ts'
3
3
  import { createComponentErrorEvent, getComponentError } from './error-event.ts'
4
4
  import { invariant } from './invariant.ts'
5
5
  import type { RemixElement, RemixNode } from './jsx.ts'
6
+ import type { ElementFunction } from './element-function.ts'
6
7
  import type { FrameHandle } from './component.ts'
7
8
  import type { Scheduler, VirtualRoot } from './vdom.ts'
8
9
  import { createRangeRoot, createRoot } from './vdom.ts'
9
10
  import { diffNodes } from './diff-dom.ts'
10
11
  import { createStyleManager, type StyleManager } from '../style/index.ts'
11
12
  import { findFlushMarker, type FlushKind } from './stream-protocol.ts'
13
+ import { getDocumentModulePreloader } from './module-preloader.ts'
14
+ import { unwrapFrameResolution } from './frame-resolution.ts'
15
+ import {
16
+ disposeClientEntryBoundary,
17
+ getClientEntryBoundaryOwner,
18
+ setClientEntryBoundaryOwner,
19
+ type ClientEntryIdentity,
20
+ } from './client-entry-boundary.ts'
12
21
 
13
22
  type FrameRoot = [Comment, Comment] | Element | Document | DocumentFragment
14
23
 
@@ -62,24 +71,71 @@ export type LoadModule = (moduleUrl: string, exportName: string) => Promise<Func
62
71
  * Resolves content for a browser-loaded frame.
63
72
  *
64
73
  * @param src Source string from the `<Frame src>` prop.
65
- * @param signal Abort signal for the active frame load or reload.
66
- * @param target Optional name of the frame being reloaded.
67
- * @returns HTML, a stream of HTML bytes, or Remix node content to render into the frame.
74
+ * @param options Information about the active frame load or form submission.
75
+ * @returns Frame content or a response whose body should be rendered into the frame.
68
76
  */
69
77
  export type ResolveFrame = (
70
78
  src: string,
71
- signal?: AbortSignal,
72
- target?: string,
73
- ) => Promise<FrameContent> | FrameContent
79
+ options?: ResolveFrameOptions,
80
+ ) => Promise<FrameResolution> | FrameResolution
81
+
82
+ /**
83
+ * Information available while resolving browser-loaded frame content.
84
+ */
85
+ export interface ResolveFrameOptions {
86
+ /** Optional name of the frame being loaded or reloaded. */
87
+ target?: string
88
+ /** Form values submitted to the frame source for a non-GET submission. */
89
+ formData?: FormData
90
+ /** HTTP method selected by the form and its submitter. */
91
+ method?: string
92
+ /** Form encoding selected by the form and its submitter. */
93
+ encType?: string
94
+ /** Aborts the reload when the navigation that started it is cancelled. */
95
+ signal?: AbortSignal
96
+ }
74
97
 
75
98
  type InternalFrameContent = FrameContent | DocumentFragment
76
99
 
100
+ type FrameReloadOptions = Omit<ResolveFrameOptions, 'target'>
101
+
102
+ type FrameReloadResult = {
103
+ signal: AbortSignal
104
+ redirectedTo?: string
105
+ }
106
+
77
107
  type FrameTemplateListener = (fragment: DocumentFragment) => void
78
108
 
79
109
  const bufferedFrameTemplates = new Map<string, DocumentFragment[]>()
80
110
  const frameTemplateListeners = new Map<string, Set<FrameTemplateListener>>()
81
111
  const DOCTYPE_PATTERN = /<!doctype(?:\s[^>]*)?>/gi
82
112
 
113
+ function createLinkedAbortController(
114
+ first: AbortSignal,
115
+ second?: AbortSignal,
116
+ ): { controller: AbortController; disconnect: () => void } {
117
+ let controller = new AbortController()
118
+ let signals = second ? [first, second] : [first]
119
+ let abort = () => controller.abort()
120
+
121
+ for (let signal of signals) {
122
+ if (signal.aborted) {
123
+ controller.abort()
124
+ break
125
+ }
126
+ signal.addEventListener('abort', abort, { once: true })
127
+ }
128
+
129
+ return {
130
+ controller,
131
+ disconnect() {
132
+ for (let signal of signals) {
133
+ signal.removeEventListener('abort', abort)
134
+ }
135
+ },
136
+ }
137
+ }
138
+
83
139
  function stripDoctypeMarkup(html: string): string {
84
140
  return html.replace(DOCTYPE_PATTERN, '')
85
141
  }
@@ -98,7 +154,11 @@ function syncElementAttributes(target: Element, source: Element) {
98
154
  }
99
155
  }
100
156
 
157
+ const FRAME_RUNTIME = Symbol('FrameRuntime')
158
+
101
159
  export type FrameRuntime = {
160
+ [FRAME_RUNTIME]: true
161
+ canResolveFrames?: boolean
102
162
  topFrame?: FrameHandle
103
163
  errorTarget: EventTarget
104
164
  loadModule: LoadModule
@@ -106,11 +166,34 @@ export type FrameRuntime = {
106
166
  pendingClientEntries: PendingClientEntries
107
167
  scheduler: Scheduler
108
168
  styleManager: StyleManager
109
- data: RmxData
110
- moduleCache: Map<string, Function>
111
- moduleLoads: Map<string, Promise<Function | undefined>>
169
+ moduleCache: Map<string, ElementFunction>
170
+ moduleLoads: Map<string, Promise<ElementFunction | undefined>>
112
171
  frameInstances: WeakMap<Comment, Frame>
113
172
  namedFrames: Map<string, FrameHandle>
173
+ serverFrameReload: { signal: AbortSignal } | undefined
174
+ reloadForNavigation?: (options?: FrameReloadOptions) => Promise<FrameReloadResult>
175
+ }
176
+
177
+ export function isFrameRuntime(value: unknown): value is FrameRuntime {
178
+ return isRecord(value) && Reflect.get(value, FRAME_RUNTIME) === true
179
+ }
180
+
181
+ /**
182
+ * Reloads a frame and returns response metadata used by the navigation runtime.
183
+ *
184
+ * @param frame Frame handle to reload.
185
+ * @param options Form submission metadata and cancellation signal.
186
+ * @returns The reload signal and final response URL, when redirected.
187
+ */
188
+ export function reloadFrameForNavigation(
189
+ frame: FrameHandle,
190
+ options?: FrameReloadOptions,
191
+ ): Promise<FrameReloadResult> {
192
+ let runtime = frame.$runtime
193
+ invariant(isFrameRuntime(runtime), 'Expected a frame runtime')
194
+ let reload = runtime.reloadForNavigation
195
+ invariant(reload, 'Expected frame runtime to support navigation reloads')
196
+ return reload(options)
114
197
  }
115
198
 
116
199
  export type FrameContext = {
@@ -123,13 +206,15 @@ export type FrameContext = {
123
206
  frame: FrameHandle
124
207
  styleManager: StyleManager
125
208
  data: RmxData
126
- moduleCache: Map<string, Function>
127
- moduleLoads: Map<string, Promise<Function | undefined>>
209
+ moduleCache: Map<string, ElementFunction>
210
+ moduleLoads: Map<string, Promise<ElementFunction | undefined>>
128
211
  frameInstances: WeakMap<Comment, Frame>
129
212
  namedFrames: Map<string, FrameHandle>
213
+ lifecycleSignal: AbortSignal
130
214
  regionTailRef?: ChildNode | null
131
215
  regionParent?: ParentNode | null
132
216
  signal?: AbortSignal
217
+ isActiveModulePreload?: (node: Node) => boolean
133
218
  }
134
219
 
135
220
  type FrameInit = {
@@ -144,8 +229,8 @@ type FrameInit = {
144
229
  styleManager?: StyleManager
145
230
  marker?: FrameMarkerData
146
231
  data: RmxData
147
- moduleCache: Map<string, Function>
148
- moduleLoads: Map<string, Promise<Function | undefined>>
232
+ moduleCache: Map<string, ElementFunction>
233
+ moduleLoads: Map<string, Promise<ElementFunction | undefined>>
149
234
  frameInstances: WeakMap<Comment, Frame>
150
235
  namedFrames: Map<string, FrameHandle>
151
236
  }
@@ -156,6 +241,11 @@ export type Frame = {
156
241
  flush: () => void
157
242
  clearPendingTemplateWatch: () => void
158
243
  isDisplayingResolvedContent: () => boolean
244
+ beginClientFrameReloadForAncestorReload: (signal: AbortSignal) => {
245
+ controller: AbortController
246
+ complete: () => void
247
+ }
248
+ cancelReload: () => void
159
249
  startInheritedReload: (signal?: AbortSignal) => void
160
250
  updateMarker: (marker: FrameMarkerData, options?: RenderOptions) => Promise<void>
161
251
  renderMarkerContent: (
@@ -163,6 +253,7 @@ export type Frame = {
163
253
  content: InternalFrameContent,
164
254
  options?: RenderOptions,
165
255
  ) => Promise<void>
256
+ matchesIdentity: (src: string, name: string | undefined) => boolean
166
257
  dispose: () => void
167
258
  handle: FrameHandle
168
259
  }
@@ -172,13 +263,24 @@ type RenderOptions = {
172
263
  initialHydrationTracker?: InitialHydrationTracker
173
264
  signal?: AbortSignal
174
265
  contentStatus?: 'pending' | 'resolved'
266
+ data?: RmxData
267
+ }
268
+
269
+ type ActiveRenderOptions = RenderOptions & {
270
+ data: RmxData
175
271
  }
176
272
 
177
273
  export function createFrame(root: FrameRoot, init: FrameInit): Frame {
178
274
  let container = createContainer(root)
179
275
  let contentRoot: VirtualRoot | undefined
180
276
  let reloadController: AbortController | undefined
277
+ // The style registry is document-level and shared by every frame; only the
278
+ // frame that created it (the runtime root) disposes it.
279
+ let ownsStyleManager = !init.styleManager
280
+ let reloadAbortUnsubscribe: (() => void) | undefined
281
+ let reloadKind: 'direct' | 'ancestor' | undefined
181
282
  let styleManager = init.styleManager ?? createStyleManager()
283
+ let modulePreloader = getDocumentModulePreloader(container.doc)
182
284
  let currentMarker = init.marker
183
285
  let displayedContentStatus: 'pending' | 'resolved' = init.marker?.status ?? 'resolved'
184
286
  let pendingTemplateMarkerId: string | undefined
@@ -186,36 +288,24 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
186
288
  let pendingTemplateUnsubscribe: (() => void) | undefined
187
289
  let inheritedReloadPending = false
188
290
  let inheritedReloadAbortUnsubscribe: (() => void) | undefined
291
+ let disposed = false
292
+ let lifecycleController = new AbortController()
293
+
294
+ if (isDocumentNode(container.root)) {
295
+ modulePreloader.adoptInitialPreloadLinks(container.root)
296
+ } else {
297
+ modulePreloader.consumePreloadLinks(container.root)
298
+ }
189
299
 
190
300
  // Merge any rmx-data found in the current document once at startup.
191
301
  mergeRmxDataFromDocument(init.data, container.doc)
192
302
 
193
- let runtime = createFrameRuntime({ ...init, styleManager })
303
+ let runtime = createFrameRuntime({ ...init, styleManager, reloadForNavigation: reload })
194
304
 
195
305
  let frame = createFrameHandle({
196
306
  src: init.src,
197
307
  $runtime: runtime,
198
- reload: async () => {
199
- reloadController?.abort()
200
- let controller = new AbortController()
201
- reloadController = controller
202
- frame.dispatchEvent(new Event('reloadStart'))
203
- startSubFrameInheritedReloads(getContentNodes(), controller.signal)
204
- try {
205
- let content = await init.resolveFrame(frame.src, controller.signal, frameName)
206
- if (reloadController !== controller || controller.signal.aborted) return controller.signal
207
- await render(content, { signal: controller.signal })
208
- return controller.signal
209
- } catch (error) {
210
- if (reloadController !== controller || controller.signal.aborted) return controller.signal
211
- init.errorTarget.dispatchEvent(createComponentErrorEvent(error))
212
- throw error
213
- } finally {
214
- if (reloadController === controller) {
215
- frame.dispatchEvent(new Event('reloadComplete'))
216
- }
217
- }
218
- },
308
+ reload: async () => (await reload()).signal,
219
309
  replace: async (content: FrameContent) => {
220
310
  await render(content)
221
311
  },
@@ -241,18 +331,47 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
241
331
  moduleLoads: init.moduleLoads,
242
332
  frameInstances: init.frameInstances,
243
333
  namedFrames: init.namedFrames,
334
+ lifecycleSignal: lifecycleController.signal,
244
335
  regionTailRef: container.regionTailRef,
245
336
  regionParent: container.regionParent,
246
337
  }
247
338
 
248
339
  async function render(content: InternalFrameContent, options?: RenderOptions): Promise<void> {
249
- if (options?.signal?.aborted) return
340
+ if (disposed || lifecycleController.signal.aborted || options?.signal?.aborted) return
341
+ let ownsData = options?.data === undefined
342
+ let renderOptions: ActiveRenderOptions = {
343
+ ...options,
344
+ data: options?.data ?? {},
345
+ }
346
+
347
+ try {
348
+ await renderContent(content, renderOptions)
349
+ } finally {
350
+ if (ownsData) clearRmxData(renderOptions.data)
351
+ }
352
+ }
353
+
354
+ async function renderContent(
355
+ content: InternalFrameContent,
356
+ options: ActiveRenderOptions,
357
+ ): Promise<void> {
358
+ if (isRenderAborted(options.signal)) return
250
359
 
251
360
  if (content instanceof ReadableStream) {
252
- await renderFrameStream(content, container.doc, async (html, flushKind) => {
253
- if (options?.signal?.aborted) return
254
- await render(html, { ...options, flushKind })
255
- })
361
+ let linkedAbort = createLinkedAbortController(lifecycleController.signal, options.signal)
362
+ try {
363
+ await renderFrameStream(
364
+ content,
365
+ container.doc,
366
+ async (html, flushKind) => {
367
+ if (isRenderAborted(options.signal)) return
368
+ await render(html, { ...options, flushKind })
369
+ },
370
+ linkedAbort.controller.signal,
371
+ )
372
+ } finally {
373
+ linkedAbort.disconnect()
374
+ }
256
375
  return
257
376
  }
258
377
 
@@ -265,9 +384,9 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
265
384
  contentRoot = createFrameContentRoot()
266
385
  }
267
386
 
268
- if (options?.signal?.aborted) return
387
+ if (isRenderAborted(options.signal)) return
269
388
  contentRoot.render(content)
270
- displayedContentStatus = options?.contentStatus ?? 'resolved'
389
+ displayedContentStatus = options.contentStatus ?? 'resolved'
271
390
  return
272
391
  }
273
392
 
@@ -293,61 +412,86 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
293
412
  let isFullDocumentReload =
294
413
  container.root instanceof Document &&
295
414
  htmlContent !== undefined &&
296
- options?.flushKind === 'document'
415
+ options.flushKind === 'document'
297
416
 
298
417
  if (isFullDocumentReload && htmlContent !== undefined) {
299
418
  let parsed = new DOMParser().parseFromString(htmlContent, 'text/html')
300
- mergeRmxDataFromDocument(context.data, parsed)
301
- context.styleManager.replaceServerStyles(
419
+ modulePreloader.consumePreloadLinks(parsed)
420
+ let responseData = options.data
421
+ mergeRmxDataFromDocument(responseData, parsed)
422
+ let responseContext = { ...context, data: responseData }
423
+ context.styleManager.adoptServerStyles(
302
424
  collectFrameServerStyleTags(createElementContainer(parsed)),
303
425
  )
304
426
 
305
427
  syncElementAttributes(container.doc.documentElement, parsed.documentElement)
306
428
 
307
429
  diffNodes([container.doc.head], [parsed.head], {
308
- ...context,
430
+ ...responseContext,
309
431
  regionParent: container.doc.documentElement,
310
432
  regionTailRef: null,
311
- signal: options?.signal,
433
+ signal: options.signal,
434
+ isActiveModulePreload: modulePreloader.hasActivePreloads()
435
+ ? modulePreloader.isActivePreload
436
+ : undefined,
312
437
  })
313
438
  diffNodes([container.doc.body], [parsed.body], {
314
- ...context,
439
+ ...responseContext,
315
440
  regionParent: container.doc.documentElement,
316
441
  regionTailRef: null,
317
- signal: options?.signal,
442
+ signal: options.signal,
318
443
  })
319
444
 
320
445
  let bodyContainer = createElementContainer(container.doc.body)
321
- if (options?.signal?.aborted) return
322
- scheduleHydrationInContainer(bodyContainer, context, options?.initialHydrationTracker)
323
- await createSubFrames(bodyContainer.childNodes, context, options)
324
- displayedContentStatus = options?.contentStatus ?? 'resolved'
446
+ if (isRenderAborted(options.signal)) return
447
+ scheduleHydrationInContainer(
448
+ bodyContainer,
449
+ responseContext,
450
+ options.initialHydrationTracker,
451
+ options.signal,
452
+ )
453
+ await createSubFrames(bodyContainer.childNodes, responseContext, options)
454
+ if (isRenderAborted(options.signal)) return
455
+ displayedContentStatus = options.contentStatus ?? 'resolved'
325
456
  return
326
457
  }
327
458
 
328
459
  let fragment =
329
460
  htmlContent !== undefined ? createFragmentFromString(container.doc, htmlContent) : content
330
- context.styleManager.replaceServerStyles(
461
+ modulePreloader.consumePreloadLinks(fragment)
462
+ context.styleManager.adoptServerStyles(
331
463
  collectFrameServerStyleTags(createElementContainer(fragment)),
332
464
  )
333
465
  removeEmptyHeads(fragment)
334
- mergeRmxDataFromFragment(context.data, fragment)
466
+ let responseData = options.data
467
+ mergeRmxDataFromFragment(responseData, fragment)
468
+ let responseContext = { ...context, data: responseData }
335
469
 
336
470
  let nextContainer = createContainer(fragment)
337
471
 
338
- if (options?.signal?.aborted) return
472
+ if (isRenderAborted(options.signal)) return
339
473
 
340
474
  diffNodes(container.childNodes, Array.from(nextContainer.childNodes), {
341
- ...context,
475
+ ...responseContext,
342
476
  regionTailRef: container.regionTailRef,
343
477
  regionParent: container.regionParent,
344
- signal: options?.signal,
478
+ signal: options.signal,
345
479
  })
346
480
 
347
- if (options?.signal?.aborted) return
348
- scheduleHydrationInContainer(container, context, options?.initialHydrationTracker)
349
- await createSubFrames(container.childNodes, context, options)
350
- displayedContentStatus = options?.contentStatus ?? 'resolved'
481
+ if (isRenderAborted(options.signal)) return
482
+ scheduleHydrationInContainer(
483
+ container,
484
+ responseContext,
485
+ options.initialHydrationTracker,
486
+ options.signal,
487
+ )
488
+ await createSubFrames(container.childNodes, responseContext, options)
489
+ if (isRenderAborted(options.signal)) return
490
+ displayedContentStatus = options.contentStatus ?? 'resolved'
491
+ }
492
+
493
+ function isRenderAborted(signal?: AbortSignal): boolean {
494
+ return disposed || lifecycleController.signal.aborted || signal?.aborted === true
351
495
  }
352
496
 
353
497
  function createFrameContentRoot(): VirtualRoot {
@@ -390,21 +534,36 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
390
534
  async function hydrateInitial(): Promise<void> {
391
535
  let initialHydrationTracker = createInitialHydrationTracker()
392
536
 
393
- context.styleManager.replaceServerStyles(collectFrameServerStyleTags(container))
394
- await createSubFrames(container.childNodes, context)
537
+ context.styleManager.adoptServerStyles(collectFrameServerStyleTags(container))
538
+ let subFramesReady = createSubFrames(container.childNodes, context)
395
539
  scheduleHydrationInContainer(container, context, initialHydrationTracker)
396
540
 
397
- if (currentMarker?.status === 'pending') {
398
- await watchPendingFrameTemplate(currentMarker, initialHydrationTracker)
399
- }
541
+ try {
542
+ await subFramesReady
543
+
544
+ if (disposed || context.lifecycleSignal.aborted) return
545
+
546
+ if (currentMarker?.status === 'pending') {
547
+ await watchPendingFrameTemplate(currentMarker, initialHydrationTracker)
548
+ }
400
549
 
401
- initialHydrationTracker.finalize()
402
- await initialHydrationTracker.ready()
550
+ initialHydrationTracker.finalize()
551
+ await initialHydrationTracker.ready()
552
+ } finally {
553
+ clearRmxData(context.data)
554
+ }
403
555
  }
404
556
 
405
557
  function dispose(): void {
558
+ if (disposed) return
559
+ disposed = true
560
+ lifecycleController.abort()
561
+ clearRmxData(context.data)
406
562
  reloadController?.abort()
407
563
  reloadController = undefined
564
+ reloadAbortUnsubscribe?.()
565
+ reloadAbortUnsubscribe = undefined
566
+ reloadKind = undefined
408
567
  contentRoot?.dispose()
409
568
  contentRoot = undefined
410
569
  clearPendingFrameTemplateWatch()
@@ -414,7 +573,9 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
414
573
 
415
574
  // Dispose sub-frames recursively.
416
575
  disposeSubFrames(container.childNodes, context)
417
- context.styleManager.dispose()
576
+ if (ownsStyleManager) {
577
+ context.styleManager.dispose()
578
+ }
418
579
 
419
580
  if (frameName) {
420
581
  if (init.namedFrames.get(frameName) === frame) {
@@ -431,15 +592,18 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
431
592
  flush: () => context.scheduler.dequeue(),
432
593
  clearPendingTemplateWatch: clearPendingFrameTemplateWatch,
433
594
  isDisplayingResolvedContent: () => displayedContentStatus === 'resolved',
595
+ beginClientFrameReloadForAncestorReload,
596
+ cancelReload,
434
597
  startInheritedReload,
435
598
  updateMarker,
436
599
  renderMarkerContent,
600
+ matchesIdentity: (src, name) => !disposed && frame.src === src && frameName === name,
437
601
  dispose,
438
602
  handle: frame,
439
603
  }
440
604
 
441
605
  async function updateMarker(marker: FrameMarkerData, options?: RenderOptions): Promise<void> {
442
- if (options?.signal?.aborted) return
606
+ if (disposed || context.lifecycleSignal.aborted || options?.signal?.aborted) return
443
607
  let previousMarker = currentMarker
444
608
  let isInheritedReload = previousMarker !== undefined && previousMarker.id !== marker.id
445
609
  currentMarker = marker
@@ -472,7 +636,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
472
636
  content: InternalFrameContent,
473
637
  options?: RenderOptions,
474
638
  ): Promise<void> {
475
- if (options?.signal?.aborted) return
639
+ if (disposed || context.lifecycleSignal.aborted || options?.signal?.aborted) return
476
640
  let previousMarker = currentMarker
477
641
  let isInheritedReload = previousMarker !== undefined && previousMarker.id !== marker.id
478
642
  currentMarker = marker
@@ -484,11 +648,119 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
484
648
  clearPendingFrameTemplateWatch()
485
649
  await render(content, { ...options, contentStatus: 'resolved' })
486
650
 
487
- if (isInheritedReload && !options?.signal?.aborted) {
651
+ if (
652
+ isInheritedReload &&
653
+ !disposed &&
654
+ !context.lifecycleSignal.aborted &&
655
+ !options?.signal?.aborted
656
+ ) {
488
657
  completeInheritedReload()
489
658
  }
490
659
  }
491
660
 
661
+ async function reload(options?: FrameReloadOptions): Promise<FrameReloadResult> {
662
+ let controller = startReload(options?.signal)
663
+ return await resolveAndRenderReload(controller, options)
664
+ }
665
+
666
+ function startReload(signal?: AbortSignal): AbortController {
667
+ let controller = replaceReloadController(signal)
668
+ reloadKind = 'direct'
669
+ frame.dispatchEvent(new Event('reloadStart'))
670
+ startSubFrameInheritedReloads(getContentNodes(), controller.signal)
671
+ return controller
672
+ }
673
+
674
+ function beginClientFrameReloadForAncestorReload(signal: AbortSignal): {
675
+ controller: AbortController
676
+ complete: () => void
677
+ } {
678
+ let inheritedReloadStarted = reuseInheritedReloadStart()
679
+ let continuingAncestorReload = reloadKind === 'ancestor'
680
+ let controller = replaceReloadController(signal)
681
+ reloadKind = 'ancestor'
682
+
683
+ if (!inheritedReloadStarted && !continuingAncestorReload) {
684
+ frame.dispatchEvent(new Event('reloadStart'))
685
+ startSubFrameInheritedReloads(getContentNodes(), controller.signal)
686
+ }
687
+
688
+ return {
689
+ controller,
690
+ complete: () => completeReload(controller),
691
+ }
692
+ }
693
+
694
+ function cancelReload(): void {
695
+ let controller = reloadController
696
+ if (!controller) return
697
+ controller.abort()
698
+ completeReload(controller)
699
+ }
700
+
701
+ function replaceReloadController(signal?: AbortSignal): AbortController {
702
+ reloadController?.abort()
703
+ reloadAbortUnsubscribe?.()
704
+ reloadAbortUnsubscribe = undefined
705
+
706
+ let controller = new AbortController()
707
+ reloadController = controller
708
+
709
+ if (signal) {
710
+ if (signal.aborted) {
711
+ controller.abort()
712
+ } else {
713
+ let abort = () => controller.abort()
714
+ signal.addEventListener('abort', abort, { once: true })
715
+ reloadAbortUnsubscribe = () => signal.removeEventListener('abort', abort)
716
+ }
717
+ }
718
+
719
+ return controller
720
+ }
721
+
722
+ async function resolveAndRenderReload(
723
+ controller: AbortController,
724
+ options?: FrameReloadOptions,
725
+ ): Promise<FrameReloadResult> {
726
+ try {
727
+ let resolution = await init.resolveFrame(frame.src, {
728
+ ...options,
729
+ signal: controller.signal,
730
+ target: frameName,
731
+ })
732
+ if (reloadController !== controller || controller.signal.aborted) {
733
+ return { signal: controller.signal }
734
+ }
735
+ let { content, redirectedTo } = await unwrapFrameResolution(resolution)
736
+ if (reloadController !== controller || controller.signal.aborted) {
737
+ return { signal: controller.signal }
738
+ }
739
+ await render(content, { signal: controller.signal })
740
+ return {
741
+ signal: controller.signal,
742
+ redirectedTo:
743
+ reloadController === controller && !controller.signal.aborted ? redirectedTo : undefined,
744
+ }
745
+ } catch (error) {
746
+ if (reloadController !== controller || controller.signal.aborted) {
747
+ return { signal: controller.signal }
748
+ }
749
+ init.errorTarget.dispatchEvent(createComponentErrorEvent(error))
750
+ throw error
751
+ } finally {
752
+ completeReload(controller)
753
+ }
754
+ }
755
+
756
+ function completeReload(controller: AbortController): void {
757
+ if (reloadController !== controller || reloadKind === undefined) return
758
+ reloadAbortUnsubscribe?.()
759
+ reloadAbortUnsubscribe = undefined
760
+ reloadKind = undefined
761
+ frame.dispatchEvent(new Event('reloadComplete'))
762
+ }
763
+
492
764
  function startInheritedReload(signal?: AbortSignal): void {
493
765
  if (signal?.aborted) return
494
766
  if (!inheritedReloadPending) {
@@ -508,6 +780,14 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
508
780
  }
509
781
  }
510
782
 
783
+ function reuseInheritedReloadStart(): boolean {
784
+ if (!inheritedReloadPending) return false
785
+ inheritedReloadPending = false
786
+ inheritedReloadAbortUnsubscribe?.()
787
+ inheritedReloadAbortUnsubscribe = undefined
788
+ return true
789
+ }
790
+
511
791
  function completeInheritedReload(): void {
512
792
  if (!inheritedReloadPending) return
513
793
  inheritedReloadPending = false
@@ -525,7 +805,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
525
805
  if (isFrameStart(node)) {
526
806
  let end = findEndMarker(node, isFrameStart, isFrameEnd)
527
807
  context.frameInstances.get(node)?.startInheritedReload(signal)
528
- i = nodes.indexOf(end)
808
+ i = findMarkerRangeEndIndex(nodes, end, i)
529
809
  continue
530
810
  }
531
811
 
@@ -549,7 +829,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
549
829
  signal?: AbortSignal,
550
830
  onResolved?: () => void,
551
831
  ): Promise<void> {
552
- if (signal?.aborted) return
832
+ if (disposed || context.lifecycleSignal.aborted || signal?.aborted) return
553
833
  if (pendingTemplateMarkerId === marker.id) return
554
834
 
555
835
  clearPendingFrameTemplateWatch()
@@ -559,11 +839,11 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
559
839
  if (early) {
560
840
  clearPendingFrameTemplateWatch()
561
841
  await render(early, { initialHydrationTracker, signal, contentStatus: 'resolved' })
562
- if (!signal?.aborted) onResolved?.()
842
+ if (!disposed && !context.lifecycleSignal.aborted && !signal?.aborted) onResolved?.()
563
843
  return
564
844
  }
565
845
 
566
- if (signal?.aborted) {
846
+ if (disposed || context.lifecycleSignal.aborted || signal?.aborted) {
567
847
  clearPendingFrameTemplateWatch()
568
848
  return
569
849
  }
@@ -571,11 +851,11 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
571
851
  let observer = setupTemplateObserver()
572
852
  pendingTemplateObserver = observer
573
853
  let unsubscribe = subscribeFrameTemplate(marker.id, async (fragment) => {
574
- if (signal?.aborted) return
854
+ if (disposed || context.lifecycleSignal.aborted || signal?.aborted) return
575
855
  if (pendingTemplateMarkerId !== marker.id) return
576
856
  clearPendingFrameTemplateWatch()
577
857
  await render(fragment, { signal, contentStatus: 'resolved' })
578
- if (!signal?.aborted) onResolved?.()
858
+ if (!disposed && !context.lifecycleSignal.aborted && !signal?.aborted) onResolved?.()
579
859
  })
580
860
  pendingTemplateUnsubscribe = unsubscribe
581
861
 
@@ -593,7 +873,7 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
593
873
  if (buffered) {
594
874
  clearPendingFrameTemplateWatch()
595
875
  await render(buffered, { initialHydrationTracker, signal, contentStatus: 'resolved' })
596
- if (!signal?.aborted) onResolved?.()
876
+ if (!disposed && !context.lifecycleSignal.aborted && !signal?.aborted) onResolved?.()
597
877
  }
598
878
  }
599
879
  }
@@ -606,13 +886,14 @@ export function createFrameRuntime(init: {
606
886
  pendingClientEntries: PendingClientEntries
607
887
  scheduler: Scheduler
608
888
  styleManager: StyleManager
609
- data: RmxData
610
- moduleCache: Map<string, Function>
611
- moduleLoads: Map<string, Promise<Function | undefined>>
889
+ moduleCache: Map<string, ElementFunction>
890
+ moduleLoads: Map<string, Promise<ElementFunction | undefined>>
612
891
  frameInstances: WeakMap<Comment, Frame>
613
892
  namedFrames: Map<string, FrameHandle>
893
+ reloadForNavigation?: (options?: FrameReloadOptions) => Promise<FrameReloadResult>
614
894
  }): FrameRuntime {
615
895
  return {
896
+ [FRAME_RUNTIME]: true,
616
897
  topFrame: init.topFrame,
617
898
  errorTarget: init.errorTarget,
618
899
  loadModule: init.loadModule,
@@ -620,11 +901,12 @@ export function createFrameRuntime(init: {
620
901
  pendingClientEntries: init.pendingClientEntries,
621
902
  scheduler: init.scheduler,
622
903
  styleManager: init.styleManager,
623
- data: init.data,
624
904
  moduleCache: init.moduleCache,
625
905
  moduleLoads: init.moduleLoads,
626
906
  frameInstances: init.frameInstances,
627
907
  namedFrames: init.namedFrames,
908
+ serverFrameReload: undefined,
909
+ reloadForNavigation: init.reloadForNavigation,
628
910
  }
629
911
  }
630
912
 
@@ -689,6 +971,11 @@ function mergeRmxDataFromFragment(into: RmxData, fragment: DocumentFragment): vo
689
971
  }
690
972
  }
691
973
 
974
+ function clearRmxData(data: RmxData): void {
975
+ delete data.h
976
+ delete data.f
977
+ }
978
+
692
979
  function removeEmptyHeads(fragment: DocumentFragment): void {
693
980
  let heads = Array.from(fragment.querySelectorAll('head'))
694
981
  for (let head of heads) {
@@ -715,7 +1002,7 @@ function collectOwnedServerStyleTags(nodes: Node[], styles: HTMLStyleElement[]):
715
1002
 
716
1003
  if (isFrameStart(node)) {
717
1004
  let end = findEndMarker(node, isFrameStart, isFrameEnd)
718
- i = nodes.indexOf(end)
1005
+ i = findMarkerRangeEndIndex(nodes, end, i)
719
1006
  continue
720
1007
  }
721
1008
 
@@ -724,7 +1011,7 @@ function collectOwnedServerStyleTags(nodes: Node[], styles: HTMLStyleElement[]):
724
1011
  continue
725
1012
  }
726
1013
 
727
- if (node instanceof Element || node instanceof Document || node instanceof DocumentFragment) {
1014
+ if (node.childNodes.length > 0) {
728
1015
  collectOwnedServerStyleTags(Array.from(node.childNodes), styles)
729
1016
  }
730
1017
  }
@@ -763,6 +1050,7 @@ function scheduleHydrationInContainer(
763
1050
  container: FrameContainer,
764
1051
  context: FrameContext,
765
1052
  initialHydrationTracker?: InitialHydrationTracker,
1053
+ signal?: AbortSignal,
766
1054
  ): void {
767
1055
  let hydrationMarkers = findHydrationMarkers(container)
768
1056
  if (hydrationMarkers.length === 0) return
@@ -773,7 +1061,7 @@ function scheduleHydrationInContainer(
773
1061
  for (let marker of hydrationMarkers) {
774
1062
  let entry = hydrationData[marker.id]
775
1063
  if (!entry) continue
776
- scheduleHydrationMarker(marker, entry, context, initialHydrationTracker)
1064
+ scheduleHydrationMarker(marker, entry, context, initialHydrationTracker, signal)
777
1065
  }
778
1066
  }
779
1067
 
@@ -782,49 +1070,74 @@ function scheduleHydrationMarker(
782
1070
  entry: HydrationData,
783
1071
  context: FrameContext,
784
1072
  initialHydrationTracker?: InitialHydrationTracker,
1073
+ signal?: AbortSignal,
785
1074
  ): void {
1075
+ if (signal?.aborted || context.lifecycleSignal.aborted) return
1076
+
786
1077
  let done = initialHydrationTracker?.track()
787
1078
  let key = `${entry.moduleUrl}#${entry.exportName}`
1079
+ let identity: ClientEntryIdentity = {
1080
+ moduleUrl: entry.moduleUrl,
1081
+ exportName: entry.exportName,
1082
+ }
1083
+ let props: Record<string, unknown> | undefined = entry.props
1084
+ let completed = false
1085
+
1086
+ let complete = () => {
1087
+ if (completed) return
1088
+ completed = true
1089
+ props = undefined
1090
+ signal?.removeEventListener('abort', complete)
1091
+ context.lifecycleSignal.removeEventListener('abort', complete)
1092
+ done?.()
1093
+ }
788
1094
 
789
- let hydrateWithComponent = (component: Function) => {
1095
+ signal?.addEventListener('abort', complete, { once: true })
1096
+ context.lifecycleSignal.addEventListener('abort', complete, { once: true })
1097
+
1098
+ let hydrateWithComponent = (component: ElementFunction) => {
1099
+ if (signal?.aborted || context.lifecycleSignal.aborted) return
790
1100
  if (!isHydrationMarkerLive(marker, context)) return
791
- let vElement = createElement(component, entry.props)
1101
+ if (!props) return
1102
+ let vElement = createElement(component, props)
792
1103
  context.pendingClientEntries.set(marker.start, [marker.end, vElement])
793
- hydrateRegion(vElement, marker.start, marker.end, context)
1104
+ hydrateRegion(vElement, marker.start, marker.end, identity, context, signal)
794
1105
  }
795
1106
 
796
1107
  let cached = context.moduleCache.get(key)
797
1108
  if (cached) {
798
1109
  hydrateWithComponent(cached)
799
- done?.()
1110
+ complete()
800
1111
  return
801
1112
  }
802
1113
 
803
- getOrStartModuleLoad(key, entry, marker.id, context)
1114
+ getOrStartModuleLoad(key, identity, marker.id, context)
804
1115
  .then((component) => {
805
1116
  if (component) {
806
1117
  hydrateWithComponent(component)
807
1118
  }
808
1119
  })
809
1120
  .finally(() => {
810
- done?.()
1121
+ complete()
811
1122
  })
812
1123
  }
813
1124
 
814
1125
  function getOrStartModuleLoad(
815
1126
  key: string,
816
- entry: HydrationData,
1127
+ identity: ClientEntryIdentity,
817
1128
  markerId: string,
818
1129
  context: FrameContext,
819
- ): Promise<Function | undefined> {
1130
+ ): Promise<ElementFunction | undefined> {
820
1131
  let inFlight = context.moduleLoads.get(key)
821
1132
  if (inFlight) return inFlight
822
1133
 
823
1134
  let loadPromise = (async () => {
824
1135
  try {
825
- let mod = await context.loadModule(entry.moduleUrl, entry.exportName)
826
- if (typeof mod !== 'function') {
827
- throw new Error(`Export "${entry.exportName}" from "${entry.moduleUrl}" is not a function`)
1136
+ let mod = await context.loadModule(identity.moduleUrl, identity.exportName)
1137
+ if (!isElementFunction(mod)) {
1138
+ throw new Error(
1139
+ `Export "${identity.exportName}" from "${identity.moduleUrl}" is not a function`,
1140
+ )
828
1141
  }
829
1142
  context.moduleCache.set(key, mod)
830
1143
  return mod
@@ -840,9 +1153,14 @@ function getOrStartModuleLoad(
840
1153
  return loadPromise
841
1154
  }
842
1155
 
843
- function createElement(component: Function, props: Record<string, unknown>): RemixElement {
1156
+ function createElement(component: ElementFunction, props: Record<string, unknown>): RemixElement {
844
1157
  let revivedProps = reviveSerializedValue(props)
845
- return jsx(component, revivedProps as any)
1158
+ invariant(isRecord(revivedProps), 'Expected revived component props to be an object')
1159
+ return jsx(component, revivedProps)
1160
+ }
1161
+
1162
+ function isElementFunction(value: unknown): value is ElementFunction {
1163
+ return typeof value === 'function'
846
1164
  }
847
1165
 
848
1166
  function reviveSerializedValue(value: unknown): unknown {
@@ -853,18 +1171,19 @@ function reviveSerializedValue(value: unknown): unknown {
853
1171
  return value.map((item) => reviveSerializedValue(item))
854
1172
  }
855
1173
 
856
- let record = value as Record<string, unknown>
1174
+ if (!isRecord(value)) return value
1175
+ let record = value
857
1176
 
858
1177
  if (record.$rmxFrame === true) {
859
1178
  let props = reviveSerializedObject(record.props)
860
1179
  let key = reviveSerializedValue(record.key)
861
- return jsx(Frame as any, props as any, key as any)
1180
+ return jsx(Frame, props, key)
862
1181
  }
863
1182
 
864
1183
  if (record.$rmx === true && typeof record.type === 'string') {
865
1184
  let props = reviveSerializedObject(record.props)
866
1185
  let key = reviveSerializedValue(record.key)
867
- return jsx(record.type as any, props as any, key as any)
1186
+ return jsx(record.type, props, key)
868
1187
  }
869
1188
 
870
1189
  let revived: Record<string, unknown> = {}
@@ -878,22 +1197,49 @@ function reviveSerializedObject(value: unknown): Record<string, unknown> {
878
1197
  if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
879
1198
  let revived = reviveSerializedValue(value)
880
1199
  if (!revived || typeof revived !== 'object' || Array.isArray(revived)) return {}
881
- return revived as Record<string, unknown>
1200
+ return isRecord(revived) ? revived : {}
1201
+ }
1202
+
1203
+ function isRecord(value: unknown): value is Record<string, unknown> {
1204
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
882
1205
  }
883
1206
 
884
1207
  function hydrateRegion(
885
1208
  vElement: RemixElement,
886
1209
  start: Comment,
887
1210
  end: Comment,
1211
+ identity: ClientEntryIdentity,
888
1212
  context: FrameContext,
1213
+ signal?: AbortSignal,
889
1214
  ): void {
1215
+ if (signal?.aborted) return
1216
+
890
1217
  context.pendingClientEntries.delete(start)
891
1218
 
892
1219
  // The same marker can be discovered by overlapping hydration passes
893
1220
  // (for example, document root + nested frame root). Reuse the existing
894
1221
  // virtual root instead of redefining the marker property.
895
- if (isHydratedVirtualRootMarker(start)) {
896
- start.$rmx.render(vElement)
1222
+ let owner = getClientEntryBoundaryOwner(start)
1223
+ if (owner) {
1224
+ if (!signal) {
1225
+ owner.root.render(vElement)
1226
+ return
1227
+ }
1228
+
1229
+ let frameRuntime = context.frame.$runtime
1230
+ invariant(
1231
+ isFrameRuntime(frameRuntime),
1232
+ 'Expected frame runtime while rendering a preserved client entry',
1233
+ )
1234
+
1235
+ let previousServerFrameReload = frameRuntime.serverFrameReload
1236
+
1237
+ frameRuntime.serverFrameReload = { signal }
1238
+ try {
1239
+ owner.root.render(vElement)
1240
+ } finally {
1241
+ frameRuntime.serverFrameReload = previousServerFrameReload
1242
+ }
897
1243
  return
898
1244
  }
899
1245
 
@@ -907,7 +1253,7 @@ function hydrateRegion(
907
1253
  context.errorTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event)))
908
1254
  })
909
1255
 
910
- Object.defineProperty(start, '$rmx', { value: root, enumerable: false })
1256
+ setClientEntryBoundaryOwner(start, identity, root)
911
1257
  root.render(vElement)
912
1258
  }
913
1259
 
@@ -948,6 +1294,7 @@ async function createSubFrames(
948
1294
  resolveFrame: context.resolveFrame,
949
1295
  pendingClientEntries: context.pendingClientEntries,
950
1296
  scheduler: context.scheduler,
1297
+ styleManager: context.styleManager,
951
1298
  data: context.data,
952
1299
  moduleCache: context.moduleCache,
953
1300
  moduleLoads: context.moduleLoads,
@@ -958,7 +1305,7 @@ async function createSubFrames(
958
1305
  }
959
1306
  }
960
1307
 
961
- i = nodes.indexOf(end)
1308
+ i = findMarkerRangeEndIndex(nodes, end, i)
962
1309
  continue
963
1310
  }
964
1311
 
@@ -996,10 +1343,9 @@ function removeVirtualRoots(nodes: Node[]): void {
996
1343
  for (let i = 0; i < nodes.length; i++) {
997
1344
  let node = nodes[i]
998
1345
 
999
- if (isHydratedVirtualRootMarker(node)) {
1000
- node.$rmx.dispose()
1346
+ if (isCommentNode(node) && isHydrationStart(node) && disposeClientEntryBoundary(node)) {
1001
1347
  let end = findEndMarker(node, isHydrationStart, isHydrationEnd)
1002
- i = nodes.indexOf(end)
1348
+ i = findMarkerRangeEndIndex(nodes, end, i)
1003
1349
  continue
1004
1350
  }
1005
1351
 
@@ -1020,7 +1366,7 @@ function disposeSubFrames(nodes: Node[], context: FrameContext): void {
1020
1366
  subFrame.dispose()
1021
1367
  context.frameInstances.delete(node)
1022
1368
  }
1023
- i = nodes.indexOf(end)
1369
+ i = findMarkerRangeEndIndex(nodes, end, i)
1024
1370
  continue
1025
1371
  }
1026
1372
 
@@ -1087,7 +1433,9 @@ export function publishFrameTemplate(id: string, fragment: DocumentFragment): vo
1087
1433
  }
1088
1434
 
1089
1435
  for (let listener of listeners) {
1090
- listener(fragment.cloneNode(true) as DocumentFragment)
1436
+ let clone = fragment.cloneNode(true)
1437
+ invariant(isDocumentFragmentNode(clone), 'Expected cloned frame template fragment')
1438
+ listener(clone)
1091
1439
  }
1092
1440
  }
1093
1441
 
@@ -1186,12 +1534,22 @@ async function renderFrameStream(
1186
1534
  stream: ReadableStream<Uint8Array>,
1187
1535
  doc: Document,
1188
1536
  applyHtml: (html: string, flushKind: FlushKind) => Promise<void>,
1537
+ signal?: AbortSignal,
1189
1538
  ): Promise<void> {
1190
1539
  let reader = stream.getReader()
1191
1540
  let decoder = new TextDecoder()
1192
1541
  let buffer = ''
1193
1542
  let html = ''
1194
1543
  let appliedOnce = false
1544
+ let abort = () => {
1545
+ void reader.cancel().catch(() => {})
1546
+ }
1547
+ if (signal?.aborted) {
1548
+ await reader.cancel()
1549
+ reader.releaseLock()
1550
+ return
1551
+ }
1552
+ signal?.addEventListener('abort', abort, { once: true })
1195
1553
 
1196
1554
  try {
1197
1555
  while (true) {
@@ -1230,6 +1588,7 @@ async function renderFrameStream(
1230
1588
  await applyHtml('', 'fragment')
1231
1589
  }
1232
1590
  } finally {
1591
+ signal?.removeEventListener('abort', abort)
1233
1592
  reader.releaseLock()
1234
1593
  }
1235
1594
  }
@@ -1312,7 +1671,7 @@ function createFragmentFromString(doc: Document, content: string): DocumentFragm
1312
1671
  function isRemixNodeFrameContent(content: InternalFrameContent): content is RemixNode {
1313
1672
  return !(
1314
1673
  content instanceof ReadableStream ||
1315
- content instanceof DocumentFragment ||
1674
+ isDocumentFragmentNode(content) ||
1316
1675
  typeof content === 'string'
1317
1676
  )
1318
1677
  }
@@ -1350,11 +1709,11 @@ function walkCommentsInNodes(nodes: Node[], cb: (comment: Comment) => void): voi
1350
1709
  // are discovered and hydrated by the nested frame instance only.
1351
1710
  if (isFrameStart(node)) {
1352
1711
  let end = findEndMarker(node, isFrameStart, isFrameEnd)
1353
- i = nodes.indexOf(end)
1712
+ i = findMarkerRangeEndIndex(nodes, end, i)
1354
1713
  continue
1355
1714
  }
1356
1715
 
1357
- if (node.nodeType === Node.COMMENT_NODE) cb(node as Comment)
1716
+ if (isCommentNode(node)) cb(node)
1358
1717
  if (node.childNodes && node.childNodes.length > 0) {
1359
1718
  walkCommentsInNodes(Array.from(node.childNodes), cb)
1360
1719
  }
@@ -1369,12 +1728,8 @@ function isHydrationEnd(node: Comment): boolean {
1369
1728
  return node.data.trim() === '/rmx:h'
1370
1729
  }
1371
1730
 
1372
- function isHydratedVirtualRootMarker(node: Node): node is VirtualRootMarker {
1373
- return node instanceof Comment && '$rmx' in node
1374
- }
1375
-
1376
1731
  function isFrameStart(node: Node): node is Comment {
1377
- return node instanceof Comment && node.data.trim().startsWith('rmx:f:')
1732
+ return isCommentNode(node) && node.data.trim().startsWith('rmx:f:')
1378
1733
  }
1379
1734
 
1380
1735
  function isFrameEnd(node: Comment): boolean {
@@ -1387,6 +1742,11 @@ function getFrameId(start: Comment): string {
1387
1742
  return trimmed.slice('rmx:f:'.length)
1388
1743
  }
1389
1744
 
1745
+ function findMarkerRangeEndIndex(nodes: Node[], end: Comment, startIndex: number): number {
1746
+ // The snapshot may not contain an end marker moved by a DOM update.
1747
+ return Math.max(startIndex, nodes.indexOf(end))
1748
+ }
1749
+
1390
1750
  function findEndMarker(
1391
1751
  start: Comment,
1392
1752
  isStart: (node: Comment) => boolean,
@@ -1396,8 +1756,8 @@ function findEndMarker(
1396
1756
  let depth = 1
1397
1757
 
1398
1758
  while (node) {
1399
- if (node.nodeType === Node.COMMENT_NODE) {
1400
- let comment = node as Comment
1759
+ if (isCommentNode(node)) {
1760
+ let comment = node
1401
1761
  if (isStart(comment)) depth++
1402
1762
  else if (isEnd(comment)) {
1403
1763
  depth--
@@ -1409,3 +1769,19 @@ function findEndMarker(
1409
1769
 
1410
1770
  throw new Error('End marker not found')
1411
1771
  }
1772
+
1773
+ function isCommentNode(node: Node | null | undefined): node is Comment {
1774
+ return node?.nodeType === Node.COMMENT_NODE
1775
+ }
1776
+
1777
+ function isDocumentNode(node: Node): node is Document {
1778
+ return node.nodeType === Node.DOCUMENT_NODE
1779
+ }
1780
+
1781
+ function isDocumentFragmentNode(value: unknown): value is DocumentFragment {
1782
+ return (
1783
+ typeof value === 'object' &&
1784
+ value !== null &&
1785
+ Reflect.get(value, 'nodeType') === Node.DOCUMENT_FRAGMENT_NODE
1786
+ )
1787
+ }