@@ -1038,6 +1038,164 @@ setModuleImports('abies.js', {
10381038 }
10391039 } ) ,
10401040
1041+ /**
1042+ * Apply a batch of patches to the DOM in a single operation.
1043+ * This reduces JS interop overhead by processing multiple patches at once.
1044+ * @param {string } patchesJson - JSON array of patch operations.
1045+ */
1046+ applyPatches : withSpan ( 'applyPatches' , async ( patchesJson ) => {
1047+ const patches = JSON . parse ( patchesJson ) ;
1048+ for ( const patch of patches ) {
1049+ switch ( patch . Type ) {
1050+ case 'SetAppContent' :
1051+ document . body . innerHTML = patch . Html ;
1052+ addEventListeners ( ) ;
1053+ window . abiesReady = true ;
1054+ break ;
1055+ case 'AddChild' : {
1056+ const parent = document . getElementById ( patch . ParentId ) ;
1057+ if ( parent ) {
1058+ parent . insertAdjacentHTML ( 'beforeend' , patch . Html ) ;
1059+ // Check for new event handlers in the added content
1060+ const addedElements = parent . querySelectorAll ( '[data-event-click], [data-event-input], [data-event-change], [data-event-submit], [data-event-keydown], [data-event-keyup], [data-event-keypress], [data-event-focus], [data-event-blur], [data-event-mouseenter], [data-event-mouseleave], [data-event-mouseover], [data-event-mouseout], [data-event-mousedown], [data-event-mouseup], [data-event-dblclick], [data-event-contextmenu]' ) ;
1061+ for ( const el of addedElements ) {
1062+ for ( const attr of el . attributes ) {
1063+ if ( attr . name . startsWith ( 'data-event-' ) ) {
1064+ ensureEventListener ( attr . name . substring ( 'data-event-' . length ) ) ;
1065+ }
1066+ }
1067+ }
1068+ } else {
1069+ console . error ( `Parent node with ID ${ patch . ParentId } not found.` ) ;
1070+ }
1071+ break ;
1072+ }
1073+ case 'RemoveChild' : {
1074+ const parent = document . getElementById ( patch . ParentId ) ;
1075+ const child = document . getElementById ( patch . ChildId ) ;
1076+ if ( parent && child ) {
1077+ parent . removeChild ( child ) ;
1078+ } else {
1079+ console . error ( `RemoveChild failed: parent=${ patch . ParentId } , child=${ patch . ChildId } ` ) ;
1080+ }
1081+ break ;
1082+ }
1083+ case 'ReplaceChild' : {
1084+ const oldNode = document . getElementById ( patch . TargetId ) ;
1085+ if ( oldNode && oldNode . parentNode ) {
1086+ const template = document . createElement ( 'template' ) ;
1087+ template . innerHTML = patch . Html ;
1088+ const newNode = template . content . firstChild ;
1089+ if ( newNode ) {
1090+ oldNode . parentNode . replaceChild ( newNode , oldNode ) ;
1091+ // Check for new event handlers
1092+ if ( newNode . querySelectorAll ) {
1093+ const elements = newNode . querySelectorAll ( '[data-event-click], [data-event-input], [data-event-change], [data-event-submit], [data-event-keydown], [data-event-keyup], [data-event-keypress], [data-event-focus], [data-event-blur], [data-event-mouseenter], [data-event-mouseleave], [data-event-mouseover], [data-event-mouseout], [data-event-mousedown], [data-event-mouseup], [data-event-dblclick], [data-event-contextmenu]' ) ;
1094+ for ( const el of elements ) {
1095+ for ( const attr of el . attributes ) {
1096+ if ( attr . name . startsWith ( 'data-event-' ) ) {
1097+ ensureEventListener ( attr . name . substring ( 'data-event-' . length ) ) ;
1098+ }
1099+ }
1100+ }
1101+ }
1102+ }
1103+ } else {
1104+ console . error ( `ReplaceChild failed: target=${ patch . TargetId } not found.` ) ;
1105+ }
1106+ break ;
1107+ }
1108+ case 'UpdateAttribute' : {
1109+ const node = document . getElementById ( patch . TargetId ) ;
1110+ if ( node ) {
1111+ const lower = patch . AttrName . toLowerCase ( ) ;
1112+ const isBooleanAttr = (
1113+ lower === 'disabled' || lower === 'checked' || lower === 'selected' || lower === 'readonly' ||
1114+ lower === 'multiple' || lower === 'required' || lower === 'autofocus' || lower === 'inert' ||
1115+ lower === 'hidden' || lower === 'open' || lower === 'loop' || lower === 'muted' || lower === 'controls'
1116+ ) ;
1117+ if ( isBooleanAttr ) {
1118+ try { if ( lower in node ) node [ lower ] = true ; } catch { /* ignore */ }
1119+ }
1120+ if ( lower === 'value' && 'value' in node ) {
1121+ node . value = patch . AttrValue ;
1122+ }
1123+ node . setAttribute ( patch . AttrName , patch . AttrValue ) ;
1124+ if ( patch . AttrName . startsWith ( 'data-event-' ) ) {
1125+ ensureEventListener ( patch . AttrName . substring ( 'data-event-' . length ) ) ;
1126+ }
1127+ } else {
1128+ console . error ( `UpdateAttribute failed: node=${ patch . TargetId } not found.` ) ;
1129+ }
1130+ break ;
1131+ }
1132+ case 'AddAttribute' : {
1133+ const node = document . getElementById ( patch . TargetId ) ;
1134+ if ( node ) {
1135+ const lower = patch . AttrName . toLowerCase ( ) ;
1136+ const isBooleanAttr = (
1137+ lower === 'disabled' || lower === 'checked' || lower === 'selected' || lower === 'readonly' ||
1138+ lower === 'multiple' || lower === 'required' || lower === 'autofocus' || lower === 'inert' ||
1139+ lower === 'hidden' || lower === 'open' || lower === 'loop' || lower === 'muted' || lower === 'controls'
1140+ ) ;
1141+ if ( isBooleanAttr ) {
1142+ try { if ( lower in node ) node [ lower ] = true ; } catch { /* ignore */ }
1143+ }
1144+ if ( lower === 'value' && 'value' in node ) {
1145+ node . value = patch . AttrValue ;
1146+ }
1147+ node . setAttribute ( patch . AttrName , patch . AttrValue ) ;
1148+ if ( patch . AttrName . startsWith ( 'data-event-' ) ) {
1149+ ensureEventListener ( patch . AttrName . substring ( 'data-event-' . length ) ) ;
1150+ }
1151+ } else {
1152+ console . error ( `AddAttribute failed: node=${ patch . TargetId } not found.` ) ;
1153+ }
1154+ break ;
1155+ }
1156+ case 'RemoveAttribute' : {
1157+ const node = document . getElementById ( patch . TargetId ) ;
1158+ if ( node ) {
1159+ const lower = patch . AttrName . toLowerCase ( ) ;
1160+ const isBooleanAttr = (
1161+ lower === 'disabled' || lower === 'checked' || lower === 'selected' || lower === 'readonly' ||
1162+ lower === 'multiple' || lower === 'required' || lower === 'autofocus' || lower === 'inert' ||
1163+ lower === 'hidden' || lower === 'open' || lower === 'loop' || lower === 'muted' || lower === 'controls'
1164+ ) ;
1165+ node . removeAttribute ( patch . AttrName ) ;
1166+ if ( isBooleanAttr ) {
1167+ try { if ( lower in node ) node [ lower ] = false ; } catch { /* ignore */ }
1168+ }
1169+ } else {
1170+ console . error ( `RemoveAttribute failed: node=${ patch . TargetId } not found.` ) ;
1171+ }
1172+ break ;
1173+ }
1174+ case 'UpdateText' : {
1175+ const node = document . getElementById ( patch . TargetId ) ;
1176+ if ( node ) {
1177+ node . textContent = patch . Text ;
1178+ } else {
1179+ console . error ( `UpdateText failed: node=${ patch . TargetId } not found.` ) ;
1180+ }
1181+ break ;
1182+ }
1183+ case 'UpdateTextWithId' : {
1184+ const node = document . getElementById ( patch . TargetId ) ;
1185+ if ( node ) {
1186+ node . textContent = patch . Text ;
1187+ node . setAttribute ( 'id' , patch . NewId ) ;
1188+ } else {
1189+ console . error ( `UpdateTextWithId failed: node=${ patch . TargetId } not found.` ) ;
1190+ }
1191+ break ;
1192+ }
1193+ default :
1194+ console . error ( `Unknown patch type: ${ patch . Type } ` ) ;
1195+ }
1196+ }
1197+ } ) ,
1198+
10411199 setLocalStorage : withSpan ( 'setLocalStorage' , async ( key , value ) => {
10421200 localStorage . setItem ( key , value ) ;
10431201 } ) ,
0 commit comments