@@ -430,6 +430,168 @@ describe('dial queue', () => {
430430 expect ( components . transportManager . dial . callCount ) . to . equal ( 1 , 'should have coalesced multiple dials to same dial' )
431431 } )
432432
433+ describe ( 'addressDialTimeout' , ( ) => {
434+ // helper: returns a transport stub that hangs until the signal fires
435+ function hangUntilAborted ( options ?: { signal ?: AbortSignal } ) : Promise < Connection > {
436+ return new Promise < Connection > ( ( _resolve , reject ) => {
437+ options ?. signal ?. addEventListener ( 'abort' , ( ) => { reject ( options . signal ?. reason ) } , { once : true } )
438+ } )
439+ }
440+
441+ it ( 'should move on to the next address when one address dial hangs' , async ( ) => {
442+ const connection = stubInterface < Connection > ( )
443+ const addressDialTimeout = 100
444+ const dialledAddrs : string [ ] = [ ]
445+
446+ components . transportManager . dialTransportForMultiaddr . returns ( stubInterface < Transport > ( ) )
447+ components . transportManager . dial . callsFake ( async ( ma , options ) => {
448+ dialledAddrs . push ( ma . toString ( ) )
449+
450+ if ( ma . toString ( ) . includes ( '1231' ) ) {
451+ return hangUntilAborted ( options )
452+ }
453+
454+ return connection
455+ } )
456+
457+ dialer = new DialQueue ( components , {
458+ addressDialTimeout,
459+ dialTimeout : 5000
460+ } )
461+
462+ const start = Date . now ( )
463+ const conn = await dialer . dial ( [
464+ multiaddr ( '/ip4/127.0.0.1/tcp/1231' ) ,
465+ multiaddr ( '/ip4/127.0.0.1/tcp/1232' )
466+ ] )
467+ const elapsed = Date . now ( ) - start
468+
469+ expect ( conn ) . to . equal ( connection )
470+ // both addresses must have been attempted
471+ expect ( dialledAddrs ) . to . include ( '/ip4/127.0.0.1/tcp/1231' )
472+ expect ( dialledAddrs ) . to . include ( '/ip4/127.0.0.1/tcp/1232' )
473+ // elapsed >= addressDialTimeout (first hung) and well under dialTimeout
474+ expect ( elapsed ) . to . be . greaterThanOrEqual ( addressDialTimeout )
475+ expect ( elapsed ) . to . be . lessThan ( addressDialTimeout * 4 )
476+ } )
477+
478+ it ( 'should try all addresses when multiple dials hang sequentially before one succeeds' , async ( ) => {
479+ // This is the exact scenario from the bug report:
480+ // [/ip4/10.2.0.2, /ip4/127.0.0.1, /ip4/172.20.5.94] where only the last is reachable.
481+ // With the old code the first address consumed the entire dialTimeout.
482+ const connection = stubInterface < Connection > ( )
483+ const addressDialTimeout = 100
484+ const dialledAddrs : string [ ] = [ ]
485+ let dialsAborted = 0
486+
487+ components . transportManager . dialTransportForMultiaddr . returns ( stubInterface < Transport > ( ) )
488+ components . transportManager . dial . callsFake ( async ( ma , options ) => {
489+ const maStr = ma . toString ( )
490+ dialledAddrs . push ( maStr )
491+
492+ if ( ! maStr . includes ( '1234' ) ) {
493+ // first three addresses hang – will be cut by per-address timeout
494+ options ?. signal ?. addEventListener ( 'abort' , ( ) => { dialsAborted ++ } , { once : true } )
495+ return hangUntilAborted ( options )
496+ }
497+
498+ return connection
499+ } )
500+
501+ dialer = new DialQueue ( components , {
502+ addressDialTimeout,
503+ dialTimeout : 10_000
504+ } )
505+
506+ const conn = await dialer . dial ( [
507+ multiaddr ( '/ip4/127.0.0.1/tcp/1231' ) ,
508+ multiaddr ( '/ip4/127.0.0.1/tcp/1232' ) ,
509+ multiaddr ( '/ip4/127.0.0.1/tcp/1233' ) ,
510+ multiaddr ( '/ip4/127.0.0.1/tcp/1234' )
511+ ] )
512+
513+ expect ( conn ) . to . equal ( connection )
514+ // all four addresses were attempted in order
515+ expect ( dialledAddrs ) . to . deep . equal ( [
516+ '/ip4/127.0.0.1/tcp/1231' ,
517+ '/ip4/127.0.0.1/tcp/1232' ,
518+ '/ip4/127.0.0.1/tcp/1233' ,
519+ '/ip4/127.0.0.1/tcp/1234'
520+ ] )
521+ // per-address signal was aborted for each of the three hung dials
522+ expect ( dialsAborted ) . to . equal ( 3 )
523+ } )
524+
525+ it ( 'should throw an AggregateError when every address times out per-address before the batch timeout' , async ( ) => {
526+ const addressDialTimeout = 100
527+
528+ components . transportManager . dialTransportForMultiaddr . returns ( stubInterface < Transport > ( ) )
529+ components . transportManager . dial . callsFake ( async ( ma , options ) => hangUntilAborted ( options ) )
530+
531+ dialer = new DialQueue ( components , {
532+ addressDialTimeout,
533+ dialTimeout : 10_000 // batch timeout is far away
534+ } )
535+
536+ const err = await dialer . dial ( [
537+ multiaddr ( '/ip4/127.0.0.1/tcp/1231' ) ,
538+ multiaddr ( '/ip4/127.0.0.1/tcp/1232' )
539+ ] ) . catch ( e => e )
540+
541+ // each address timed out individually → AggregateError, not TimeoutError
542+ expect ( err ) . to . have . property ( 'name' , 'AggregateError' )
543+ } )
544+
545+ it ( 'should throw a TimeoutError when the batch timeout fires before the per-address timeout' , async ( ) => {
546+ const dialTimeout = 100
547+ const addressDialTimeout = 5000 // much longer than dialTimeout
548+
549+ components . transportManager . dialTransportForMultiaddr . returns ( stubInterface < Transport > ( ) )
550+ components . transportManager . dial . callsFake ( async ( ma , options ) => hangUntilAborted ( options ) )
551+
552+ dialer = new DialQueue ( components , {
553+ addressDialTimeout,
554+ dialTimeout
555+ } )
556+
557+ const err = await dialer . dial ( [
558+ multiaddr ( '/ip4/127.0.0.1/tcp/1231' ) ,
559+ multiaddr ( '/ip4/127.0.0.1/tcp/1232' )
560+ ] ) . catch ( e => e )
561+
562+ // batch timeout fires → name is 'TimeoutError' (not AggregateError)
563+ // Note: the error is the native DOMException from AbortSignal.timeout(),
564+ // not our custom TimeoutError class, because JobRecipient rejects with
565+ // the raw signal reason before our callback's throw can propagate.
566+ expect ( err ) . to . have . property ( 'name' , 'TimeoutError' )
567+ } )
568+
569+ it ( 'should not delay dials that succeed within the addressDialTimeout' , async ( ) => {
570+ const connection = stubInterface < Connection > ( )
571+ const addressDialTimeout = 500
572+
573+ components . transportManager . dialTransportForMultiaddr . returns ( stubInterface < Transport > ( ) )
574+ components . transportManager . dial . callsFake ( async ( ) => {
575+ await delay ( 10 ) // quick success, well within addressDialTimeout
576+ return connection
577+ } )
578+
579+ dialer = new DialQueue ( components , {
580+ addressDialTimeout,
581+ dialTimeout : 5000
582+ } )
583+
584+ const conn = await dialer . dial ( [
585+ multiaddr ( '/ip4/127.0.0.1/tcp/1231' ) ,
586+ multiaddr ( '/ip4/127.0.0.1/tcp/1232' )
587+ ] )
588+
589+ expect ( conn ) . to . equal ( connection )
590+ // first address succeeded – second address should never have been dialled
591+ expect ( components . transportManager . dial . callCount ) . to . equal ( 1 )
592+ } )
593+ } )
594+
433595 it ( 'should continue dial when new addresses are discovered' , async ( ) => {
434596 const remotePeer = peerIdFromPrivateKey ( await generateKeyPair ( 'Ed25519' ) )
435597 const ma1 = multiaddr ( `/ip6/2001:db8:1:2:3:4:5:6/tcp/123/p2p/${ remotePeer } ` )
0 commit comments