@@ -6,11 +6,106 @@ import { ChoreBits } from './enums/chore-bits.enum';
66import type { VNodeOperation } from './types/dom-vnode-operation' ;
77import type { VNode } from './vnode' ;
88
9+ /** Reusable path array to avoid allocations */
10+ const reusablePath : VNode [ ] = [ ] ;
11+
12+ /** Propagates CHILDREN dirty bits through the collected path up to the target ancestor */
13+ function propagatePath ( target : VNode ) : void {
14+ for ( let i = 0 ; i < reusablePath . length ; i ++ ) {
15+ const child = reusablePath [ i ] ;
16+ const parent = reusablePath [ i + 1 ] || target ;
17+ parent . dirty |= ChoreBits . CHILDREN ;
18+ parent . dirtyChildren ||= [ ] ;
19+ parent . dirtyChildren . push ( child ) ;
20+ }
21+ }
22+
23+ /**
24+ * Propagates dirty bits from vNode up to the specified cursorRoot. Used during diff when we know
25+ * the cursor root to merge with. Also updates cursor position if we pass through any cursors.
26+ */
27+ function propagateToCursorRoot ( vNode : VNode , cursorRoot : VNode ) : void {
28+ reusablePath . push ( vNode ) ;
29+ let current : VNode | null = vNode . parent || vNode . slotParent ;
30+
31+ while ( current ) {
32+ const isDirty = current . dirty & ChoreBits . DIRTY_MASK ;
33+ const currentIsCursor = isCursor ( current ) ;
34+
35+ // Stop when we reach the cursor root or a dirty ancestor
36+ if ( current === cursorRoot || isDirty ) {
37+ propagatePath ( current ) ;
38+ // Update cursor position if current is a cursor
39+ if ( currentIsCursor ) {
40+ const cursorData : CursorData = getCursorData ( current ) ! ;
41+ if ( cursorData . position !== current ) {
42+ cursorData . position = vNode ;
43+ }
44+ }
45+ reusablePath . length = 0 ;
46+ return ;
47+ }
48+
49+ // Update cursor position if we pass through a cursor on the way up
50+ if ( currentIsCursor ) {
51+ const cursorData : CursorData = getCursorData ( current ) ! ;
52+ if ( cursorData . position !== current ) {
53+ cursorData . position = vNode ;
54+ }
55+ }
56+
57+ reusablePath . push ( current ) ;
58+ current = current . parent || current . slotParent ;
59+ }
60+ reusablePath . length = 0 ;
61+ }
62+
63+ /**
64+ * Finds a blocking cursor or dirty ancestor and propagates dirty bits to it. Returns true if found
65+ * and attached, false if a new cursor should be created.
66+ */
67+ function findAndPropagateToBlockingCursor ( vNode : VNode ) : boolean {
68+ reusablePath . push ( vNode ) ;
69+ let current : VNode | null = vNode . parent || vNode . slotParent ;
70+
71+ while ( current ) {
72+ const isDirty = current . dirty & ChoreBits . DIRTY_MASK ;
73+ const currentIsCursor = isCursor ( current ) ;
74+ const isBlockingCursor = currentIsCursor && getCursorData ( current ) ?. isBlocking ;
75+
76+ if ( isDirty || isBlockingCursor ) {
77+ propagatePath ( current ) ;
78+ reusablePath . length = 0 ;
79+ return true ;
80+ }
81+
82+ // Found non-blocking cursor - no point looking further up
83+ if ( currentIsCursor ) {
84+ reusablePath . length = 0 ;
85+ return false ;
86+ }
87+
88+ reusablePath . push ( current ) ;
89+ current = current . parent || current . slotParent ;
90+ }
91+ reusablePath . length = 0 ;
92+ return false ;
93+ }
94+
95+ /**
96+ * Marks a vNode as dirty and propagates dirty bits up the tree.
97+ *
98+ * @param container - The container
99+ * @param vNode - The vNode to mark dirty
100+ * @param bits - The dirty bits to set
101+ * @param cursorRoot - If provided, propagate dirty bits up to this cursor root (used during diff).
102+ * If null, will search for a blocking cursor or create a new one.
103+ */
9104export function markVNodeDirty (
10105 container : Container ,
11106 vNode : VNode ,
12107 bits : ChoreBits ,
13- mergeWithParentCursor = false
108+ cursorRoot : VNode | null = null
14109) : void {
15110 const prevDirty = vNode . dirty ;
16111 vNode . dirty |= bits ;
@@ -19,28 +114,14 @@ export function markVNodeDirty(
19114 if ( isRealDirty ? prevDirty & ChoreBits . DIRTY_MASK : prevDirty ) {
20115 return ;
21116 }
22- let parent = vNode . parent || vNode . slotParent ;
23- if ( mergeWithParentCursor && isRealDirty && parent && ! parent . dirty ) {
24- let previousParent = vNode ;
25- while ( parent ) {
26- const parentWasDirty = parent . dirty & ChoreBits . DIRTY_MASK ;
27- parent . dirty |= ChoreBits . CHILDREN ;
28- parent . dirtyChildren ||= [ ] ;
29- parent . dirtyChildren . push ( previousParent ) ;
30- if ( isCursor ( parent ) ) {
31- const cursorData : CursorData = getCursorData ( parent ) ! ;
32- if ( cursorData . position !== parent ) {
33- cursorData . position = vNode ;
34- }
35- }
36- if ( parentWasDirty ) {
37- break ;
38- }
39- previousParent = parent ;
40- parent = parent . parent || parent . slotParent ;
41- }
117+ const parent = vNode . parent || vNode . slotParent ;
118+
119+ // If cursorRoot is provided, propagate up to it
120+ if ( cursorRoot && isRealDirty && parent && ! parent . dirty ) {
121+ propagateToCursorRoot ( vNode , cursorRoot ) ;
42122 return ;
43123 }
124+
44125 // We must attach to a cursor subtree if it exists
45126 if ( parent && parent . dirty & ChoreBits . DIRTY_MASK ) {
46127 if ( isRealDirty ) {
@@ -70,7 +151,12 @@ export function markVNodeDirty(
70151 }
71152 }
72153 } else if ( ! isCursor ( vNode ) ) {
73- addCursor ( container , vNode , 0 ) ;
154+ // Check if there's an existing cursor that is blocking (executing a render-blocking task)
155+ // If so, merge with it instead of creating a new cursor (single-pass find + propagate)
156+ if ( ! findAndPropagateToBlockingCursor ( vNode ) ) {
157+ // No blocking cursor found, create a new one
158+ addCursor ( container , vNode , 0 ) ;
159+ }
74160 }
75161}
76162
0 commit comments