@@ -8,6 +8,7 @@ import type { Scheduler, VirtualRoot } from './vdom.ts'
88import { createRangeRoot , createRoot } from './vdom.ts'
99import { diffNodes } from './diff-dom.ts'
1010import { createStyleManager , type StyleManager } from '../style/index.ts'
11+ import { findFlushMarker , type FlushKind } from './stream-protocol.ts'
1112
1213type FrameRoot = [ Comment , Comment ] | Element | Document | DocumentFragment
1314
@@ -64,10 +65,6 @@ function stripDoctypeMarkup(html: string): string {
6465 return html . replace ( DOCTYPE_PATTERN , '' )
6566}
6667
67- function hasRenderableHtml ( html : string ) : boolean {
68- return stripDoctypeMarkup ( html ) . trim ( ) !== ''
69- }
70-
7168function syncElementAttributes ( target : Element , source : Element ) {
7269 for ( let attribute of Array . from ( target . attributes ) ) {
7370 if ( ! source . hasAttribute ( attribute . name ) ) {
@@ -142,6 +139,7 @@ export type Frame = {
142139}
143140
144141type RenderOptions = {
142+ flushKind ?: FlushKind
145143 initialHydrationTracker ?: InitialHydrationTracker
146144 signal ?: AbortSignal
147145}
@@ -215,9 +213,9 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
215213 if ( options ?. signal ?. aborted ) return
216214
217215 if ( content instanceof ReadableStream ) {
218- await renderFrameStream ( content , container . doc , async ( html ) => {
216+ await renderFrameStream ( content , container . doc , async ( html , flushKind ) => {
219217 if ( options ?. signal ?. aborted ) return
220- await render ( html , options )
218+ await render ( html , { ... options , flushKind } )
221219 } )
222220 return
223221 }
@@ -241,12 +239,24 @@ export function createFrame(root: FrameRoot, init: FrameInit): Frame {
241239 contentRoot = undefined
242240 }
243241
242+ if ( typeof content === 'string' ) {
243+ let flushed = await consumeFlushBatches ( content , async ( html , flushKind ) => {
244+ await render ( html , { ...options , flushKind } )
245+ } )
246+ if ( flushed . applied ) {
247+ if ( flushed . remainder !== '' ) {
248+ await render ( flushed . remainder , { ...options , flushKind : 'fragment' } )
249+ }
250+ return
251+ }
252+ }
253+
244254 let htmlContent = typeof content === 'string' ? stripDoctypeMarkup ( content ) : undefined
245255
246256 let isFullDocumentReload =
247257 container . root instanceof Document &&
248258 htmlContent !== undefined &&
249- isFullDocumentHtml ( htmlContent )
259+ options ?. flushKind === 'document'
250260
251261 if ( isFullDocumentReload && htmlContent !== undefined ) {
252262 let parsed = new DOMParser ( ) . parseFromString ( htmlContent , 'text/html' )
@@ -982,13 +992,12 @@ function extractTemplatesFromBuffer(
982992async function renderFrameStream (
983993 stream : ReadableStream < Uint8Array > ,
984994 doc : Document ,
985- applyHtml : ( html : string ) => Promise < void > ,
995+ applyHtml : ( html : string , flushKind : FlushKind ) => Promise < void > ,
986996) : Promise < void > {
987997 let reader = stream . getReader ( )
988998 let decoder = new TextDecoder ( )
989999 let buffer = ''
9901000 let html = ''
991- let appliedLength = 0
9921001 let appliedOnce = false
9931002
9941003 try {
@@ -1002,19 +1011,9 @@ async function renderFrameStream(
10021011
10031012 if ( parsed . html !== '' ) {
10041013 html += parsed . html
1005- // A doctype or whitespace prelude can arrive in its own chunk. Wait
1006- // until there is actual frame content before applying the stream.
1007- if ( ! hasRenderableHtml ( html ) ) {
1008- continue
1009- }
1010-
1011- let htmlMarkers = collectHtmlMarkerSummary ( html )
1012- if ( ! hasBalancedMarkerSummary ( htmlMarkers ) ) {
1013- continue
1014- }
1015- await applyHtml ( html )
1016- appliedLength = html . length
1017- appliedOnce = true
1014+ let flushed = await consumeFlushBatches ( html , applyHtml )
1015+ appliedOnce = flushed . applied || appliedOnce
1016+ html = flushed . remainder
10181017 }
10191018 }
10201019
@@ -1027,23 +1026,40 @@ async function renderFrameStream(
10271026 buffer = ''
10281027 }
10291028
1030- let hasHtmlToApply = hasRenderableHtml ( html )
1031-
1032- if ( hasHtmlToApply && html . length > appliedLength ) {
1033- await applyHtml ( html )
1029+ if ( html !== '' ) {
1030+ await applyHtml ( html , 'fragment' )
10341031 appliedOnce = true
10351032 }
10361033
10371034 // A frame stream can legitimately resolve to empty content. Ensure the
10381035 // existing frame region is cleared instead of treated as a no-op.
1039- if ( ! hasHtmlToApply && ! appliedOnce ) {
1040- await applyHtml ( '' )
1036+ if ( html === '' && ! appliedOnce ) {
1037+ await applyHtml ( '' , 'fragment' )
10411038 }
10421039 } finally {
10431040 reader . releaseLock ( )
10441041 }
10451042}
10461043
1044+ async function consumeFlushBatches (
1045+ html : string ,
1046+ applyHtml : ( html : string , flushKind : FlushKind ) => Promise < void > ,
1047+ ) : Promise < { applied : boolean ; remainder : string } > {
1048+ let applied = false
1049+ let cursor = 0
1050+ let marker = findFlushMarker ( html , cursor )
1051+
1052+ while ( marker ) {
1053+ let batch = html . slice ( cursor , marker . index )
1054+ await applyHtml ( batch , marker . kind )
1055+ applied = true
1056+ cursor = marker . endIndex
1057+ marker = findFlushMarker ( html , cursor )
1058+ }
1059+
1060+ return { applied, remainder : html . slice ( cursor ) }
1061+ }
1062+
10471063type FrameContainer = {
10481064 doc : Document
10491065 root : ParentNode
@@ -1108,11 +1124,6 @@ function isRemixNodeFrameContent(content: InternalFrameContent): content is Remi
11081124 )
11091125}
11101126
1111- function isFullDocumentHtml ( content : string ) : boolean {
1112- let trimmed = content . trimStart ( )
1113- return / ^ < ! d o c t y p e h t m l \b / i. test ( trimmed ) || / ^ < h t m l [ \s > ] / i. test ( trimmed )
1114- }
1115-
11161127type HydrationMarker = {
11171128 id : string
11181129 start : Comment
@@ -1206,17 +1217,3 @@ function findEndMarker(
12061217 throw new Error ( 'End marker not found' )
12071218}
12081219
1209- function collectHtmlMarkerSummary ( html : string ) : Record < string , number > {
1210- return {
1211- frameStarts : html . match ( / < ! - - \s * r m x : f : / g) ?. length ?? 0 ,
1212- frameEnds : html . match ( / < ! - - \s * \/ r m x : f \s * - - > / g) ?. length ?? 0 ,
1213- hydrationStarts : html . match ( / < ! - - \s * r m x : h : / g) ?. length ?? 0 ,
1214- hydrationEnds : html . match ( / < ! - - \s * \/ r m x : h \s * - - > / g) ?. length ?? 0 ,
1215- }
1216- }
1217-
1218- function hasBalancedMarkerSummary ( summary : Record < string , number > ) : boolean {
1219- return (
1220- summary . frameStarts === summary . frameEnds && summary . hydrationStarts === summary . hydrationEnds
1221- )
1222- }
0 commit comments