@@ -545,13 +545,24 @@ fn session_thread(
545545 }
546546
547547 // Run event loop while bridge work or async ESM
548- // evaluation is still pending.
549- let event_loop_status =
550- if pending. len ( ) > 0
551- || execution:: has_pending_module_evaluation ( )
552- || execution:: has_pending_script_evaluation ( )
553- || !deferred_queue. lock ( ) . unwrap ( ) . is_empty ( )
554- {
548+ // evaluation is still pending. For ESM modules (mode != 0),
549+ // always enter the event loop even if no pending promises
550+ // are visible yet — the module body may have registered
551+ // timers, stdin listeners, or child_process handles that
552+ // need event loop pumping to deliver their callbacks.
553+ let should_enter_event_loop =
554+ pending. len ( ) > 0
555+ || execution:: has_pending_module_evaluation ( )
556+ || execution:: has_pending_script_evaluation ( )
557+ || !deferred_queue. lock ( ) . unwrap ( ) . is_empty ( )
558+ || mode != 0 ; // Always pump for ESM modules
559+ let event_loop_status = if should_enter_event_loop {
560+ eprintln ! ( "[v8-runtime] entering event loop: pending={} module_eval={} script_eval={} deferred={} esm={}" ,
561+ pending. len( ) ,
562+ execution:: has_pending_module_evaluation( ) ,
563+ execution:: has_pending_script_evaluation( ) ,
564+ !deferred_queue. lock( ) . unwrap( ) . is_empty( ) ,
565+ mode != 0 ) ;
555566 let scope = & mut v8:: HandleScope :: new ( iso) ;
556567 let ctx = v8:: Local :: new ( scope, & exec_context) ;
557568 let scope = & mut v8:: ContextScope :: new ( scope, ctx) ;
@@ -566,7 +577,7 @@ fn session_thread(
566577 EventLoopStatus :: Completed
567578 } ;
568579
569- let terminated = matches ! ( event_loop_status, EventLoopStatus :: Terminated ) ;
580+ let mut terminated = matches ! ( event_loop_status, EventLoopStatus :: Terminated ) ;
570581 if let EventLoopStatus :: Failed ( next_code, next_error) = event_loop_status {
571582 code = next_code;
572583 error = Some ( next_error) ;
@@ -587,6 +598,62 @@ fn session_thread(
587598 }
588599 }
589600
601+ // For ESM modules: call _waitForActiveHandles() to keep
602+ // the session alive while handles (timers, child processes,
603+ // stdin listeners) are active. This creates a pending promise
604+ // that the event loop pumps until all handles resolve.
605+ if !terminated && mode != 0 && error. is_none ( ) {
606+ // Phase 1: call _waitForActiveHandles() to register a pending promise
607+ {
608+ let scope = & mut v8:: HandleScope :: new ( iso) ;
609+ let ctx = v8:: Local :: new ( scope, & exec_context) ;
610+ let scope = & mut v8:: ContextScope :: new ( scope, ctx) ;
611+ let global = ctx. global ( scope) ;
612+ let key = v8:: String :: new ( scope, "_waitForActiveHandles" ) . unwrap ( ) ;
613+ if let Some ( func) = global. get ( scope, key. into ( ) ) {
614+ if func. is_function ( ) {
615+ let func = v8:: Local :: < v8:: Function > :: try_from ( func) . unwrap ( ) ;
616+ let recv = v8:: undefined ( scope) . into ( ) ;
617+ if let Some ( result) = func. call ( scope, recv, & [ ] ) {
618+ if result. is_promise ( ) {
619+ let promise = v8:: Local :: < v8:: Promise > :: try_from ( result) . unwrap ( ) ;
620+ eprintln ! ( "[v8-runtime] _waitForActiveHandles promise state: {:?}" , promise. state( ) ) ;
621+ if promise. state ( ) == v8:: PromiseState :: Pending {
622+ execution:: set_pending_script_evaluation ( scope, promise) ;
623+ }
624+ }
625+ }
626+ }
627+ }
628+ }
629+
630+ // Phase 2: pump event loop for active handles
631+ if pending. len ( ) > 0
632+ || execution:: has_pending_script_evaluation ( )
633+ || !deferred_queue. lock ( ) . unwrap ( ) . is_empty ( )
634+ {
635+ eprintln ! ( "[v8-runtime] pumping event loop for ESM active handles" ) ;
636+ let scope = & mut v8:: HandleScope :: new ( iso) ;
637+ let ctx = v8:: Local :: new ( scope, & exec_context) ;
638+ let scope = & mut v8:: ContextScope :: new ( scope, ctx) ;
639+ let event_loop_status = run_event_loop (
640+ scope,
641+ & rx,
642+ & pending,
643+ maybe_abort_rx. as_ref ( ) ,
644+ Some ( & deferred_queue) ,
645+ ) ;
646+
647+ if matches ! ( event_loop_status, EventLoopStatus :: Terminated ) {
648+ terminated = true ;
649+ }
650+ if let EventLoopStatus :: Failed ( next_code, next_error) = event_loop_status {
651+ code = next_code;
652+ error = Some ( next_error) ;
653+ }
654+ }
655+ }
656+
590657 if !terminated && mode == 0 && error. is_none ( ) {
591658 let scope = & mut v8:: HandleScope :: new ( iso) ;
592659 let ctx = v8:: Local :: new ( scope, & exec_context) ;
0 commit comments