@@ -49,6 +49,7 @@ export class SheetManager {
4949 rules : new Map ( ) ,
5050 ruleTextSet : new Set < string > ( ) ,
5151 bulkCleanupTimeout : null ,
52+ cleanupCheckTimeout : null ,
5253 metrics,
5354 classCounter : 0 ,
5455 keyframesCache : new Map ( ) ,
@@ -596,48 +597,52 @@ export class SheetManager {
596597 rulesBySheet . get ( sheetIndex ) ! . push ( { className, ruleInfo } ) ;
597598 }
598599
599- // Resolve root node for DOM checks (Document or ShadowRoot)
600- const resolveRoot = ( ) : Document | ShadowRoot | null => {
601- const firstSheet = registry . sheets [ 0 ] ?. sheet ;
602- if ( ! firstSheet ) return null ;
603- const rootNode = ( firstSheet as any ) . getRootNode
604- ? ( firstSheet as any ) . getRootNode ( )
605- : ( firstSheet . ownerDocument as Document | null ) ;
606- // Prefer ShadowRoot if available, else Document
607- return ( rootNode as ShadowRoot | Document ) || null ;
608- } ;
609-
610- const rootNode = resolveRoot ( ) ;
611-
612600 // Delete rules from each sheet (in reverse order to preserve indices)
613601 for ( const [ sheetIndex , rulesInSheet ] of rulesBySheet ) {
614602 // Sort by rule index in descending order for safe deletion
615603 rulesInSheet . sort ( ( a , b ) => b . ruleInfo . ruleIndex - a . ruleInfo . ruleIndex ) ;
616604
617605 for ( const { className, ruleInfo } of rulesInSheet ) {
618- // SAFETY: Re-check that the rule is still unused at deletion time.
619- // Between scheduling and execution a class may have been restored
620- // (refCount set to > 0). Skip such entries.
606+ // SAFETY 1: Double-check refCount is still 0
621607 const currentRefCount = registry . refCounts . get ( className ) || 0 ;
622608 if ( currentRefCount > 0 ) {
623609 // Class became active again; do not delete
624610 continue ;
625611 }
626612
627- // Ensure we delete the same RuleInfo we marked earlier to avoid
628- // accidentally deleting updated rules for the same class.
613+ // SAFETY 2: Ensure rule wasn't replaced
614+ // Between scheduling and execution a class may have been replaced with a new RuleInfo
629615 const currentInfo = registry . rules . get ( className ) ;
630- if ( currentInfo && currentInfo !== ruleInfo ) {
616+ if ( currentInfo !== ruleInfo ) {
631617 // Rule was replaced; skip deletion of the old reference
632618 continue ;
633619 }
634620
635- // Optional last-resort safety: ensure the sheet element still exists
621+ // SAFETY 3: Verify the sheet element is still valid and accessible
636622 const sheetInfo = registry . sheets [ ruleInfo . sheetIndex ] ;
637623 if ( ! sheetInfo || ! sheetInfo . sheet ) {
624+ // Sheet was removed or corrupted; skip this rule
625+ continue ;
626+ }
627+
628+ // SAFETY 4: Verify the stylesheet itself is accessible
629+ const styleSheet = sheetInfo . sheet . sheet ;
630+ if ( ! styleSheet ) {
631+ // Stylesheet not available; skip this rule
632+ continue ;
633+ }
634+
635+ // SAFETY 5: Verify rule index is still within valid range
636+ const maxRuleIndex = styleSheet . cssRules . length - 1 ;
637+ const startIdx = ruleInfo . ruleIndex ;
638+ const endIdx = ruleInfo . endRuleIndex ?? ruleInfo . ruleIndex ;
639+
640+ if ( startIdx < 0 || endIdx > maxRuleIndex || startIdx > endIdx ) {
641+ // Rule indices are out of bounds; skip this rule
638642 continue ;
639643 }
640644
645+ // All safety checks passed - proceed with deletion
641646 this . deleteRule ( registry , ruleInfo ) ;
642647 registry . rules . delete ( className ) ;
643648 registry . refCounts . delete ( className ) ;
@@ -890,9 +895,26 @@ export class SheetManager {
890895 }
891896
892897 /**
893- * Check if cleanup should be scheduled (async, non-stacking)
898+ * Schedule async cleanup check ( non-stacking)
894899 */
895900 public checkCleanupNeeded ( registry : RootRegistry ) : void {
901+ // Clear any existing check timeout to prevent stacking
902+ if ( registry . cleanupCheckTimeout ) {
903+ clearTimeout ( registry . cleanupCheckTimeout ) ;
904+ registry . cleanupCheckTimeout = null ;
905+ }
906+
907+ // Schedule the actual check with setTimeout(..., 0)
908+ registry . cleanupCheckTimeout = setTimeout ( ( ) => {
909+ this . performCleanupCheck ( registry ) ;
910+ registry . cleanupCheckTimeout = null ;
911+ } , 0 ) ;
912+ }
913+
914+ /**
915+ * Perform the actual cleanup check (called asynchronously)
916+ */
917+ private performCleanupCheck ( registry : RootRegistry ) : void {
896918 // Count unused rules (refCount = 0) - keyframes are disposed immediately
897919 const unusedRulesCount = Array . from ( registry . refCounts . values ( ) ) . filter (
898920 ( count ) => count === 0 ,
@@ -927,6 +949,12 @@ export class SheetManager {
927949 registry . bulkCleanupTimeout = null ;
928950 }
929951
952+ // Cancel any scheduled cleanup check
953+ if ( registry . cleanupCheckTimeout ) {
954+ clearTimeout ( registry . cleanupCheckTimeout ) ;
955+ registry . cleanupCheckTimeout = null ;
956+ }
957+
930958 // Remove all sheets
931959 for ( const sheet of registry . sheets ) {
932960 try {
0 commit comments