@@ -451,6 +451,23 @@ def __init__(
451451 self .stream_buffers : dict [int , bytearray ] = {}
452452 self .stream_events : dict [int , trio .Event ] = {}
453453 self ._nursery : Nursery | None = None
454+ self ._established : bool = False
455+
456+ @property
457+ def is_established (self ) -> bool :
458+ """
459+ Check if the Yamux connection is fully established and ready for streams.
460+
461+ Returns True when:
462+ - The event_started has been set
463+ - The handle_incoming task is actively running
464+ - The connection is not shutting down
465+ """
466+ return (
467+ self ._established
468+ and self .event_started .is_set ()
469+ and not self .event_shutting_down .is_set ()
470+ )
454471
455472 async def start (self ) -> None :
456473 logger .debug (f"Starting Yamux for { self .peer_id } " )
@@ -463,8 +480,12 @@ async def start(self) -> None:
463480 logger .debug (
464481 f"Yamux.start() starting handle_incoming task for { self .peer_id } "
465482 )
466- nursery .start_soon (self .handle_incoming )
483+ # Use nursery.start() to ensure handle_incoming has started
484+ # before we set event_started. This prevents race conditions
485+ # where streams are opened before the muxer is ready.
486+ await nursery .start (self ._handle_incoming_with_ready_signal )
467487 logger .debug (f"Yamux.start() setting event_started for { self .peer_id } " )
488+ self ._established = True
468489 self .event_started .set ()
469490 logger .debug (
470491 f"Yamux.start() exiting for { self .peer_id } , closing new stream channel"
@@ -691,6 +712,23 @@ async def read_stream(self, stream_id: int, n: int = -1) -> bytes:
691712 # This line should never be reached, but satisfies the type checker
692713 raise MuxedStreamEOF ("Unexpected end of read_stream" )
693714
715+ async def _handle_incoming_with_ready_signal (
716+ self , task_status : Any = trio .TASK_STATUS_IGNORED
717+ ) -> None :
718+ """
719+ Wrapper for handle_incoming that signals when the task is ready.
720+
721+ This method uses trio's task_status to signal that the handle_incoming
722+ loop is ready to process frames. This prevents race conditions where
723+ streams are opened before the muxer is ready to handle them.
724+ """
725+ logger .debug (
726+ f"Yamux _handle_incoming_with_ready_signal() starting for "
727+ f"peer { self .peer_id } "
728+ )
729+ task_status .started ()
730+ await self .handle_incoming ()
731+
694732 async def handle_incoming (self ) -> None :
695733 logger .debug (f"Yamux handle_incoming() started for peer { self .peer_id } " )
696734 while not self .event_shutting_down .is_set ():
0 commit comments