@@ -213,9 +213,9 @@ void (async () => {
213213 try { tracer = trace . getTracer ( 'Abies.JS' ) ; } catch { }
214214 // Expose OTel handle for forceFlush on page unload
215215 try {
216- window . __otel = {
217- provider,
218- exporter,
216+ window . __otel = {
217+ provider,
218+ exporter,
219219 endpoint : guessOtlp ( ) ,
220220 // Expose verbosity controls
221221 getVerbosity,
@@ -255,21 +255,21 @@ void (async () => {
255255 // currentSpan: the span that is currently active (for creating children)
256256 // activeTraceContext: persists trace context even after span ends (for fetch calls)
257257 const state = { currentSpan : null , activeTraceContext : null , pendingSpans : [ ] } ;
258-
258+
259259 function makeSpan ( name , kind = 1 , explicitParent = undefined ) {
260260 // Use explicit parent if provided, otherwise use current span, otherwise use active trace context
261261 const parent = explicitParent !== undefined ? explicitParent : ( state . currentSpan || state . activeTraceContext ) ;
262262 const traceId = parent ?. traceId || hex ( 16 ) ;
263263 const spanId = hex ( 8 ) ;
264264 return { traceId, spanId, parentSpanId : parent ?. spanId , name, kind, start : nowNs ( ) , end : null , attributes : { } } ;
265265 }
266-
266+
267267 // Batch and export spans in OTLP JSON format
268268 let exportTimer = null ;
269269 async function flushSpans ( ) {
270270 if ( state . pendingSpans . length === 0 ) return ;
271271 const spans = state . pendingSpans . splice ( 0 , state . pendingSpans . length ) ;
272-
272+
273273 // Build OTLP JSON payload
274274 const payload = {
275275 resourceSpans : [ {
@@ -297,7 +297,7 @@ void (async () => {
297297 } ]
298298 } ]
299299 } ;
300-
300+
301301 try {
302302 await fetch ( endpoint , {
303303 method : 'POST' ,
@@ -308,20 +308,20 @@ void (async () => {
308308 // Silently ignore export errors
309309 }
310310 }
311-
311+
312312 function scheduleFlush ( ) {
313313 if ( exportTimer ) return ;
314314 exportTimer = setTimeout ( ( ) => {
315315 exportTimer = null ;
316316 flushSpans ( ) ;
317317 } , 500 ) ;
318318 }
319-
319+
320320 async function exportSpan ( span ) {
321321 state . pendingSpans . push ( span ) ;
322322 scheduleFlush ( ) ;
323323 }
324-
324+
325325 // Minimal shim tracer used by existing code paths
326326 trace = {
327327 getTracer : ( ) => ( {
@@ -340,8 +340,8 @@ void (async () => {
340340 setAttribute : ( key , value ) => { s . attributes [ key ] = value ; } ,
341341 setStatus : ( ) => { } ,
342342 recordException : ( ) => { } ,
343- end : async ( ) => {
344- s . end = nowNs ( ) ;
343+ end : async ( ) => {
344+ s . end = nowNs ( ) ;
345345 state . currentSpan = prev ;
346346 // Keep activeTraceContext alive briefly for async operations (fetch)
347347 // Clear it after a short delay to allow pending fetches to inherit context
@@ -350,7 +350,7 @@ void (async () => {
350350 state . activeTraceContext = prev ;
351351 }
352352 } , 100 ) ;
353- await exportSpan ( s ) ;
353+ await exportSpan ( s ) ;
354354 }
355355 } ;
356356 }
@@ -367,23 +367,23 @@ void (async () => {
367367 const url = ( typeof input === 'string' ) ? input : input . url ;
368368 // Don't instrument OTLP export calls (would cause infinite loop)
369369 if ( / \/ o t l p \/ v 1 \/ t r a c e s $ / . test ( url ) ) return origFetch ( input , init ) ;
370-
370+
371371 const method = ( init && init . method ) || ( typeof input !== 'string' && input . method ) || 'GET' ;
372-
372+
373373 // Create HTTP span as child of current span OR active trace context
374374 // This ensures fetch calls made after span.end() still link to the trace
375375 const parent = state . currentSpan || state . activeTraceContext ;
376376 const sp = makeSpan ( `HTTP ${ method } ` , 3 /* CLIENT */ , parent ) ;
377377 sp . attributes [ 'http.method' ] = method ;
378378 sp . attributes [ 'http.url' ] = url ;
379-
379+
380380 // Build W3C traceparent header to propagate to backend
381381 const traceparent = `00-${ sp . traceId } -${ sp . spanId } -01` ;
382382 const i = init ? { ...init } : { } ;
383383 const h = new Headers ( ( i && i . headers ) || ( typeof input !== 'string' && input . headers ) || { } ) ;
384384 h . set ( 'traceparent' , traceparent ) ;
385385 i . headers = h ;
386-
386+
387387 try {
388388 const res = await origFetch ( input , i ) ;
389389 sp . attributes [ 'http.status_code' ] = res . status ;
@@ -400,9 +400,9 @@ void (async () => {
400400 } catch { }
401401
402402 // Expose OTel handle for forceFlush on page unload
403- window . __otel = {
404- provider : { forceFlush : async ( ) => { await flushSpans ( ) ; } } ,
405- exporter : { url : endpoint } ,
403+ window . __otel = {
404+ provider : { forceFlush : async ( ) => { await flushSpans ( ) ; } } ,
405+ exporter : { url : endpoint } ,
406406 endpoint,
407407 // Expose verbosity controls
408408 getVerbosity,
@@ -451,6 +451,36 @@ const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotne
451451
452452const registeredEvents = new Set ( ) ;
453453
454+ // Pre-register all common event types at startup to avoid O(n) DOM scanning
455+ // on every incremental update. These match the event types defined in Operations.cs.
456+ const COMMON_EVENT_TYPES = [
457+ // Mouse events
458+ 'click' , 'dblclick' , 'mousedown' , 'mouseup' , 'mouseover' , 'mouseout' ,
459+ 'mouseenter' , 'mouseleave' , 'mousemove' , 'contextmenu' , 'wheel' ,
460+ // Keyboard events
461+ 'keydown' , 'keyup' , 'keypress' ,
462+ // Form events
463+ 'input' , 'change' , 'submit' , 'reset' , 'focus' , 'blur' , 'invalid' , 'search' ,
464+ // Touch events
465+ 'touchstart' , 'touchend' , 'touchmove' , 'touchcancel' ,
466+ // Pointer events
467+ 'pointerdown' , 'pointerup' , 'pointermove' , 'pointercancel' ,
468+ 'pointerover' , 'pointerout' , 'pointerenter' , 'pointerleave' ,
469+ 'gotpointercapture' , 'lostpointercapture' ,
470+ // Drag events
471+ 'drag' , 'dragstart' , 'dragend' , 'dragenter' , 'dragleave' , 'dragover' , 'drop' ,
472+ // Clipboard events
473+ 'copy' , 'cut' , 'paste' ,
474+ // Media events
475+ 'play' , 'pause' , 'ended' , 'volumechange' , 'timeupdate' , 'seeking' , 'seeked' ,
476+ 'loadeddata' , 'loadedmetadata' , 'canplay' , 'canplaythrough' , 'playing' ,
477+ 'waiting' , 'stalled' , 'suspend' , 'emptied' , 'ratechange' , 'durationchange' ,
478+ // Other events
479+ 'scroll' , 'resize' , 'load' , 'error' , 'abort' , 'select' , 'toggle' ,
480+ 'animationstart' , 'animationend' , 'animationiteration' , 'animationcancel' ,
481+ 'transitionstart' , 'transitionend' , 'transitionrun' , 'transitioncancel'
482+ ] ;
483+
454484function ensureEventListener ( eventName ) {
455485 if ( registeredEvents . has ( eventName ) ) return ;
456486 // Attach to document to survive body innerHTML changes and use capture for early handling
@@ -500,14 +530,14 @@ function genericEventHandler(event) {
500530 if ( name === 'keydown' && event && event . key === 'Enter' ) {
501531 try { event . preventDefault ( ) ; } catch { /* ignore */ }
502532 }
503-
533+
504534 // Build rich UI context for tracing
505535 const tag = ( target . tagName || '' ) . toLowerCase ( ) ;
506536 const text = ( target . textContent || '' ) . trim ( ) . substring ( 0 , 50 ) ; // Truncate long text
507537 const classes = target . className || '' ;
508538 const ariaLabel = target . getAttribute ( 'aria-label' ) || '' ;
509539 const elId = target . id || '' ;
510-
540+
511541 // Build human-readable action description
512542 let action = '' ;
513543 if ( name === 'click' ) {
@@ -528,7 +558,7 @@ function genericEventHandler(event) {
528558 } else {
529559 action = `${ name } : ${ tag } ${ elId ? '#' + elId : '' } ` ;
530560 }
531-
561+
532562 const spanOptions = {
533563 attributes : {
534564 'ui.event.type' : name ,
@@ -541,7 +571,7 @@ function genericEventHandler(event) {
541571 'abies.message_id' : message
542572 }
543573 } ;
544-
574+
545575 // Use startActiveSpan if available (CDN mode) to properly set context for nested spans
546576 // This ensures FetchInstrumentation creates child spans under this UI Event
547577 if ( typeof tracer . startActiveSpan === 'function' ) {
@@ -811,27 +841,45 @@ function unsubscribe(key) {
811841}
812842
813843/**
814- * Adds event listeners to the document body for interactive elements.
844+ * Discovers and registers event listeners for any custom (non-common) event types
845+ * in the given DOM subtree. Common event types are pre-registered at startup,
846+ * so this function primarily handles rare/custom event handlers.
847+ *
848+ * Uses TreeWalker instead of querySelectorAll for better memory efficiency.
815849 */
816850function addEventListeners ( root ) {
851+ // Scan the given scope for any data-event-* attributes.
852+ // Since common events are pre-registered, this is mostly a no-op for typical apps,
853+ // but we still need to scan for custom event types in newly added HTML.
817854 const scope = root || document ;
818- // Build a list including the scope element (if Element) plus all descendants
819- const nodes = [ ] ;
820- if ( scope && scope . nodeType === 1 /* ELEMENT_NODE */ ) nodes . push ( scope ) ;
821- scope . querySelectorAll ( '*' ) . forEach ( el => {
822- nodes . push ( el ) ;
823- } ) ;
824- nodes . forEach ( el => {
825- for ( const attr of el . attributes ) {
855+
856+ // Use TreeWalker for memory-efficient iteration
857+ const walker = document . createTreeWalker ( scope , NodeFilter . SHOW_ELEMENT ) ;
858+
859+ // Include the root element itself if it's an element
860+ if ( scope . nodeType === 1 /* ELEMENT_NODE */ && scope . attributes ) {
861+ for ( const attr of scope . attributes ) {
826862 if ( attr . name . startsWith ( 'data-event-' ) ) {
827863 const name = attr . name . substring ( 'data-event-' . length ) ;
828864 ensureEventListener ( name ) ;
829865 }
830866 }
831- } ) ;
832- }
833-
834- /**
867+ }
868+
869+ // Walk descendants
870+ let el = walker . nextNode ( ) ;
871+ while ( el ) {
872+ if ( el . attributes ) {
873+ for ( const attr of el . attributes ) {
874+ if ( attr . name . startsWith ( 'data-event-' ) ) {
875+ const name = attr . name . substring ( 'data-event-' . length ) ;
876+ ensureEventListener ( name ) ;
877+ }
878+ }
879+ }
880+ el = walker . nextNode ( ) ;
881+ }
882+ } /**
835883 * Event handler for click events on elements with data-event-* attributes.
836884 * @param {Event } event - The DOM event.
837885 */
@@ -896,7 +944,7 @@ setModuleImports('abies.js', {
896944 setTitle : withSpan ( 'setTitle' , async ( title ) => {
897945 document . title = title ;
898946 } ) ,
899-
947+
900948 /**
901949 * Removes a child element from the DOM.
902950 * @param {number } parentId - The ID of the parent element.
@@ -1345,11 +1393,16 @@ setModuleImports('abies.js', {
13451393 unsubscribe ( key ) ;
13461394 }
13471395} ) ;
1348-
1396+
13491397const config = getConfig ( ) ;
13501398const exports = await getAssemblyExports ( "Abies" ) ;
13511399
13521400await runMain ( ) ; // Ensure the .NET runtime is initialized
13531401
1402+ // Pre-register all common event types now that the runtime is ready.
1403+ // This is done after runMain() to avoid TDZ issues with exports.
1404+ // Since ensureEventListener checks registeredEvents Set, this is idempotent.
1405+ COMMON_EVENT_TYPES . forEach ( ensureEventListener ) ;
1406+
13541407// Make sure any existing data-event-* attributes in the initial DOM are discovered
13551408try { addEventListeners ( ) ; } catch ( err ) { /* ignore */ }
0 commit comments